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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +10 -1
- snowflake/cli/_app/snow_connector.py +91 -37
- snowflake/cli/_app/telemetry.py +8 -4
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/_plugins/connection/commands.py +3 -2
- snowflake/cli/_plugins/git/commands.py +55 -14
- snowflake/cli/_plugins/git/manager.py +14 -6
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
- snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
- snowflake/cli/_plugins/nativeapp/manager.py +74 -144
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
- snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +17 -246
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
- snowflake/cli/_plugins/snowpark/commands.py +5 -65
- snowflake/cli/_plugins/snowpark/common.py +17 -1
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
- snowflake/cli/_plugins/sql/commands.py +1 -2
- snowflake/cli/_plugins/stage/commands.py +2 -2
- snowflake/cli/_plugins/stage/manager.py +46 -15
- snowflake/cli/_plugins/streamlit/commands.py +4 -63
- snowflake/cli/_plugins/streamlit/manager.py +13 -0
- snowflake/cli/_plugins/workspace/action_context.py +7 -0
- snowflake/cli/_plugins/workspace/commands.py +145 -32
- snowflake/cli/_plugins/workspace/manager.py +21 -4
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/decorators.py +1 -1
- snowflake/cli/api/commands/flags.py +106 -102
- snowflake/cli/api/commands/snow_typer.py +15 -6
- snowflake/cli/api/config.py +18 -5
- snowflake/cli/api/connections.py +214 -0
- snowflake/cli/api/console/abc.py +4 -2
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/application_entity.py +687 -2
- snowflake/cli/api/entities/application_package_entity.py +407 -9
- snowflake/cli/api/entities/common.py +7 -2
- snowflake/cli/api/entities/utils.py +80 -20
- snowflake/cli/api/exceptions.py +12 -2
- snowflake/cli/api/feature_flags.py +0 -2
- snowflake/cli/api/identifiers.py +3 -0
- snowflake/cli/api/project/definition.py +35 -1
- snowflake/cli/api/project/definition_conversion.py +352 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
- snowflake/cli/api/project/schemas/entities/common.py +0 -12
- snowflake/cli/api/project/schemas/identifier_model.py +2 -2
- snowflake/cli/api/project/schemas/project_definition.py +102 -43
- snowflake/cli/api/rendering/jinja.py +2 -16
- snowflake/cli/api/rendering/project_definition_templates.py +5 -1
- snowflake/cli/api/rendering/sql_templates.py +14 -4
- snowflake/cli/api/secure_path.py +13 -18
- snowflake/cli/api/secure_utils.py +90 -1
- snowflake/cli/api/sql_execution.py +13 -0
- snowflake/cli/api/utils/definition_rendering.py +7 -7
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {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="
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
snowflake/cli/api/secure_path.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
226
|
-
Requires-Dist: pyyaml==6.0.
|
|
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.
|
|
229
|
-
Requires-Dist: rich==13.
|
|
230
|
-
Requires-Dist: setuptools==
|
|
231
|
-
Requires-Dist: snowflake-connector-python[secure-local-storage]==3.12.
|
|
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.
|
|
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.
|
|
242
|
+
Requires-Dist: syrupy==4.7.1; extra == 'development'
|
|
243
243
|
Description-Content-Type: text/markdown
|
|
244
244
|
|
|
245
245
|
<!--
|