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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/snow_connector.py +18 -11
- snowflake/cli/_plugins/connection/commands.py +3 -2
- 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 +4 -6
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
- snowflake/cli/_plugins/nativeapp/manager.py +29 -58
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
- snowflake/cli/_plugins/snowpark/commands.py +5 -65
- snowflake/cli/_plugins/snowpark/common.py +17 -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 +4 -0
- snowflake/cli/_plugins/workspace/action_context.py +6 -0
- snowflake/cli/_plugins/workspace/commands.py +103 -22
- snowflake/cli/_plugins/workspace/manager.py +20 -4
- snowflake/cli/api/cli_global_context.py +6 -6
- snowflake/cli/api/commands/decorators.py +1 -1
- snowflake/cli/api/commands/flags.py +31 -12
- snowflake/cli/api/commands/snow_typer.py +9 -2
- snowflake/cli/api/config.py +17 -4
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/application_package_entity.py +296 -3
- snowflake/cli/api/entities/common.py +6 -2
- snowflake/cli/api/entities/utils.py +46 -10
- snowflake/cli/api/exceptions.py +12 -2
- snowflake/cli/api/feature_flags.py +0 -2
- snowflake/cli/api/project/definition.py +24 -1
- snowflake/cli/api/project/definition_conversion.py +194 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
- snowflake/cli/api/project/schemas/project_definition.py +1 -4
- snowflake/cli/api/rendering/jinja.py +2 -16
- snowflake/cli/api/rendering/project_definition_templates.py +1 -1
- snowflake/cli/api/rendering/sql_templates.py +7 -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 +4 -6
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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,7 @@ _YML_TEMPLATE_START = "<%"
|
|
|
24
24
|
_YML_TEMPLATE_END = "%>"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
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(
|
|
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
|
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(
|
|
@@ -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(
|
|
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.
|
|
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.
|
|
228
|
+
Requires-Dist: requirements-parser==0.11.0
|
|
229
229
|
Requires-Dist: rich==13.7.1
|
|
230
|
-
Requires-Dist: setuptools==
|
|
231
|
-
Requires-Dist: snowflake-connector-python[secure-local-storage]==3.12.
|
|
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.
|
|
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'
|