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
|
@@ -46,13 +46,12 @@ from snowflake.cli.api.output.types import (
|
|
|
46
46
|
MessageResult,
|
|
47
47
|
SingleQueryResult,
|
|
48
48
|
)
|
|
49
|
+
from snowflake.cli.api.project.definition_conversion import (
|
|
50
|
+
convert_project_definition_to_v2,
|
|
51
|
+
)
|
|
49
52
|
from snowflake.cli.api.project.schemas.entities.streamlit_entity_model import (
|
|
50
53
|
StreamlitEntityModel,
|
|
51
54
|
)
|
|
52
|
-
from snowflake.cli.api.project.schemas.project_definition import (
|
|
53
|
-
ProjectDefinition,
|
|
54
|
-
ProjectDefinitionV2,
|
|
55
|
-
)
|
|
56
55
|
|
|
57
56
|
app = SnowTyperFactory(
|
|
58
57
|
name="streamlit",
|
|
@@ -139,7 +138,7 @@ def streamlit_deploy(
|
|
|
139
138
|
raise NoProjectDefinitionError(
|
|
140
139
|
project_type="streamlit", project_root=cli_context.project_root
|
|
141
140
|
)
|
|
142
|
-
pd =
|
|
141
|
+
pd = convert_project_definition_to_v2(pd)
|
|
143
142
|
|
|
144
143
|
streamlits: Dict[str, StreamlitEntityModel] = pd.get_entities_by_type(
|
|
145
144
|
entity_type="streamlit"
|
|
@@ -171,64 +170,6 @@ def streamlit_deploy(
|
|
|
171
170
|
return MessageResult(f"Streamlit successfully deployed and available under {url}")
|
|
172
171
|
|
|
173
172
|
|
|
174
|
-
def migrate_v1_streamlit_to_v2(pd: ProjectDefinition):
|
|
175
|
-
default_env_file = "environment.yml"
|
|
176
|
-
default_pages_dir = "pages"
|
|
177
|
-
|
|
178
|
-
# Process env file
|
|
179
|
-
environment_file = pd.streamlit.env_file
|
|
180
|
-
if environment_file and not Path(environment_file).exists():
|
|
181
|
-
raise ClickException(f"Provided file {environment_file} does not exist")
|
|
182
|
-
elif environment_file is None and Path(default_env_file).exists():
|
|
183
|
-
environment_file = default_env_file
|
|
184
|
-
# Process pages dir
|
|
185
|
-
pages_dir = pd.streamlit.pages_dir
|
|
186
|
-
if pages_dir and not Path(pages_dir).exists():
|
|
187
|
-
raise ClickException(f"Provided file {pages_dir} does not exist")
|
|
188
|
-
elif pages_dir is None and Path(default_pages_dir).exists():
|
|
189
|
-
pages_dir = default_pages_dir
|
|
190
|
-
|
|
191
|
-
# Build V2 definition
|
|
192
|
-
artifacts = [
|
|
193
|
-
pd.streamlit.main_file,
|
|
194
|
-
environment_file,
|
|
195
|
-
pages_dir,
|
|
196
|
-
]
|
|
197
|
-
artifacts = [a for a in artifacts if a is not None]
|
|
198
|
-
if pd.streamlit.additional_source_files:
|
|
199
|
-
artifacts.extend(pd.streamlit.additional_source_files)
|
|
200
|
-
|
|
201
|
-
identifier = {"name": pd.streamlit.name}
|
|
202
|
-
if pd.streamlit.schema_name:
|
|
203
|
-
identifier["schema"] = pd.streamlit.schema_name
|
|
204
|
-
if pd.streamlit.database:
|
|
205
|
-
identifier["database"] = pd.streamlit.database
|
|
206
|
-
|
|
207
|
-
if pd.streamlit.name.startswith("<%") and pd.streamlit.name.endswith("%>"):
|
|
208
|
-
streamlit_name = "streamlit_entity_1"
|
|
209
|
-
else:
|
|
210
|
-
streamlit_name = pd.streamlit.name
|
|
211
|
-
|
|
212
|
-
data = {
|
|
213
|
-
"definition_version": "2",
|
|
214
|
-
"entities": {
|
|
215
|
-
streamlit_name: {
|
|
216
|
-
"type": "streamlit",
|
|
217
|
-
"identifier": identifier,
|
|
218
|
-
"title": pd.streamlit.title,
|
|
219
|
-
"query_warehouse": pd.streamlit.query_warehouse,
|
|
220
|
-
"main_file": str(pd.streamlit.main_file),
|
|
221
|
-
"pages_dir": str(pd.streamlit.pages_dir),
|
|
222
|
-
"stage": pd.streamlit.stage,
|
|
223
|
-
"artifacts": artifacts,
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
}
|
|
227
|
-
if hasattr(pd, "env") and pd.env:
|
|
228
|
-
data["env"] = {k: v for k, v in pd.env.items()}
|
|
229
|
-
return ProjectDefinitionV2(**data)
|
|
230
|
-
|
|
231
|
-
|
|
232
173
|
@app.command("get-url", requires_connection=True)
|
|
233
174
|
def get_url(
|
|
234
175
|
name: FQN = StreamlitNameArgument,
|
|
@@ -57,6 +57,10 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
57
57
|
stage_manager = StageManager()
|
|
58
58
|
for file in artifacts:
|
|
59
59
|
if file.is_dir():
|
|
60
|
+
if not any(file.iterdir()):
|
|
61
|
+
cli_console.warning(f"Skipping empty directory: {file}")
|
|
62
|
+
continue
|
|
63
|
+
|
|
60
64
|
stage_manager.put(
|
|
61
65
|
f"{file.joinpath('*')}", f"{root_location}/{file}", 4, True
|
|
62
66
|
)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from snowflake.cli.api.console.abc import AbstractConsole
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
@dataclass
|
|
@@ -8,4 +11,7 @@ class ActionContext:
|
|
|
8
11
|
An object that is passed to each action when called by WorkspaceManager
|
|
9
12
|
"""
|
|
10
13
|
|
|
14
|
+
console: AbstractConsole
|
|
11
15
|
project_root: Path
|
|
16
|
+
default_role: str
|
|
17
|
+
default_warehouse: Optional[str]
|
|
@@ -15,19 +15,27 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from textwrap import dedent
|
|
20
|
+
from typing import List, Optional
|
|
18
21
|
|
|
19
22
|
import typer
|
|
20
23
|
import yaml
|
|
21
|
-
from click import ClickException
|
|
22
24
|
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
23
|
-
from snowflake.cli._plugins.
|
|
24
|
-
|
|
25
|
+
from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
26
|
+
ForceOption,
|
|
27
|
+
ValidateOption,
|
|
28
|
+
)
|
|
25
29
|
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
|
|
26
30
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
27
31
|
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
28
32
|
from snowflake.cli.api.commands.snow_typer import SnowTyper
|
|
29
33
|
from snowflake.cli.api.entities.common import EntityActions
|
|
34
|
+
from snowflake.cli.api.exceptions import IncompatibleParametersError
|
|
30
35
|
from snowflake.cli.api.output.types import MessageResult
|
|
36
|
+
from snowflake.cli.api.project.definition_conversion import (
|
|
37
|
+
convert_project_definition_to_v2,
|
|
38
|
+
)
|
|
31
39
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
32
40
|
from snowflake.cli.api.secure_path import SecurePath
|
|
33
41
|
|
|
@@ -45,30 +53,13 @@ def migrate(
|
|
|
45
53
|
),
|
|
46
54
|
**options,
|
|
47
55
|
):
|
|
48
|
-
"""Migrates the Snowpark and Streamlit project definition files
|
|
56
|
+
"""Migrates the Snowpark and Streamlit project definition files from V1 to V2."""
|
|
49
57
|
pd = DefinitionManager().unrendered_project_definition
|
|
50
58
|
|
|
51
59
|
if pd.meets_version_requirement("2"):
|
|
52
60
|
return MessageResult("Project definition is already at version 2.")
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
if not accept_templates:
|
|
56
|
-
raise ClickException(
|
|
57
|
-
"Project definition contains templates. They may not be migrated correctly, and require manual migration."
|
|
58
|
-
"You can try again with --accept-templates option, to attempt automatic migration."
|
|
59
|
-
)
|
|
60
|
-
log.warning(
|
|
61
|
-
"Your V1 definition contains templates. We cannot guarantee the correctness of the migration."
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if pd.streamlit:
|
|
65
|
-
pd_v2 = migrate_v1_streamlit_to_v2(pd)
|
|
66
|
-
elif pd.snowpark:
|
|
67
|
-
pd_v2 = migrate_v1_snowpark_to_v2(pd)
|
|
68
|
-
else:
|
|
69
|
-
raise ValueError(
|
|
70
|
-
"Only Snowpark and Streamlit entities are supported for migration."
|
|
71
|
-
)
|
|
62
|
+
pd_v2 = convert_project_definition_to_v2(pd, accept_templates)
|
|
72
63
|
|
|
73
64
|
SecurePath("snowflake.yml").rename("snowflake_V1.yml")
|
|
74
65
|
with open("snowflake.yml", "w") as file:
|
|
@@ -111,3 +102,93 @@ def bundle(
|
|
|
111
102
|
|
|
112
103
|
bundle_map: BundleMap = ws.perform_action(entity_id, EntityActions.BUNDLE)
|
|
113
104
|
return MessageResult(f"Bundle generated at {bundle_map.deploy_root()}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@ws.command(requires_connection=True)
|
|
108
|
+
@with_project_definition()
|
|
109
|
+
def deploy(
|
|
110
|
+
entity_id: str = typer.Option(
|
|
111
|
+
help=f"""The ID of the entity you want to deploy.""",
|
|
112
|
+
),
|
|
113
|
+
# TODO The following options should be generated automatically, depending on the specified entity type
|
|
114
|
+
prune: Optional[bool] = typer.Option(
|
|
115
|
+
default=None,
|
|
116
|
+
help=f"""Whether to delete specified files from the stage if they don't exist locally. If set, the command deletes files that exist in the stage, but not in the local filesystem. This option cannot be used when paths are specified.""",
|
|
117
|
+
),
|
|
118
|
+
recursive: Optional[bool] = typer.Option(
|
|
119
|
+
None,
|
|
120
|
+
"--recursive/--no-recursive",
|
|
121
|
+
"-r",
|
|
122
|
+
help=f"""Whether to traverse and deploy files from subdirectories. If set, the command deploys all files and subdirectories; otherwise, only files in the current directory are deployed.""",
|
|
123
|
+
),
|
|
124
|
+
paths: Optional[List[Path]] = typer.Argument(
|
|
125
|
+
default=None,
|
|
126
|
+
show_default=False,
|
|
127
|
+
help=dedent(
|
|
128
|
+
f"""
|
|
129
|
+
Paths, relative to the the project root, of files or directories you want to upload to a stage. If a file is
|
|
130
|
+
specified, it must match one of the artifacts src pattern entries in snowflake.yml. If a directory is
|
|
131
|
+
specified, it will be searched for subfolders or files to deploy based on artifacts src pattern entries. If
|
|
132
|
+
unspecified, the command syncs all local changes to the stage."""
|
|
133
|
+
).strip(),
|
|
134
|
+
),
|
|
135
|
+
validate: bool = ValidateOption,
|
|
136
|
+
**options,
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Deploys the specified entity.
|
|
140
|
+
"""
|
|
141
|
+
if prune is None and recursive is None and not paths:
|
|
142
|
+
prune = True
|
|
143
|
+
recursive = True
|
|
144
|
+
else:
|
|
145
|
+
if prune is None:
|
|
146
|
+
prune = False
|
|
147
|
+
if recursive is None:
|
|
148
|
+
recursive = False
|
|
149
|
+
|
|
150
|
+
if paths and prune:
|
|
151
|
+
raise IncompatibleParametersError(["paths", "--prune"])
|
|
152
|
+
|
|
153
|
+
cli_context = get_cli_context()
|
|
154
|
+
ws = WorkspaceManager(
|
|
155
|
+
project_definition=cli_context.project_definition,
|
|
156
|
+
project_root=cli_context.project_root,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
ws.perform_action(
|
|
160
|
+
entity_id,
|
|
161
|
+
EntityActions.DEPLOY,
|
|
162
|
+
prune=prune,
|
|
163
|
+
recursive=recursive,
|
|
164
|
+
paths=paths,
|
|
165
|
+
validate=validate,
|
|
166
|
+
)
|
|
167
|
+
return MessageResult("Deployed successfully.")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@ws.command(requires_connection=True)
|
|
171
|
+
@with_project_definition()
|
|
172
|
+
def drop(
|
|
173
|
+
entity_id: str = typer.Option(
|
|
174
|
+
help=f"""The ID of the entity you want to drop.""",
|
|
175
|
+
),
|
|
176
|
+
# TODO The following options should be generated automatically, depending on the specified entity type
|
|
177
|
+
force: Optional[bool] = ForceOption,
|
|
178
|
+
**options,
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Drops the specified entity.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
cli_context = get_cli_context()
|
|
185
|
+
ws = WorkspaceManager(
|
|
186
|
+
project_definition=cli_context.project_definition,
|
|
187
|
+
project_root=cli_context.project_root,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
ws.perform_action(
|
|
191
|
+
entity_id,
|
|
192
|
+
EntityActions.DROP,
|
|
193
|
+
force_drop=force,
|
|
194
|
+
)
|
|
@@ -2,8 +2,11 @@ from pathlib import Path
|
|
|
2
2
|
from typing import Dict
|
|
3
3
|
|
|
4
4
|
from snowflake.cli._plugins.workspace.action_context import ActionContext
|
|
5
|
-
from snowflake.cli.api.
|
|
5
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
6
|
+
from snowflake.cli.api.console import cli_console as cc
|
|
7
|
+
from snowflake.cli.api.entities.common import EntityActions, get_sql_executor
|
|
6
8
|
from snowflake.cli.api.exceptions import InvalidProjectDefinitionVersionError
|
|
9
|
+
from snowflake.cli.api.project.definition import default_role
|
|
7
10
|
from snowflake.cli.api.project.schemas.entities.entities import (
|
|
8
11
|
Entity,
|
|
9
12
|
v2_entity_model_to_entity_map,
|
|
@@ -12,6 +15,7 @@ from snowflake.cli.api.project.schemas.project_definition import (
|
|
|
12
15
|
DefinitionV20,
|
|
13
16
|
ProjectDefinition,
|
|
14
17
|
)
|
|
18
|
+
from snowflake.cli.api.project.util import to_identifier
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class WorkspaceManager:
|
|
@@ -27,6 +31,13 @@ class WorkspaceManager:
|
|
|
27
31
|
self._entities_cache: Dict[str, Entity] = {}
|
|
28
32
|
self._project_definition: DefinitionV20 = project_definition
|
|
29
33
|
self._project_root = project_root
|
|
34
|
+
self._default_role = default_role()
|
|
35
|
+
if self._default_role is None:
|
|
36
|
+
self._default_role = get_sql_executor().current_role()
|
|
37
|
+
self.default_warehouse = None
|
|
38
|
+
cli_context = get_cli_context()
|
|
39
|
+
if cli_context.connection.warehouse:
|
|
40
|
+
self.default_warehouse = to_identifier(cli_context.connection.warehouse)
|
|
30
41
|
|
|
31
42
|
def get_entity(self, entity_id: str):
|
|
32
43
|
"""
|
|
@@ -42,14 +53,19 @@ class WorkspaceManager:
|
|
|
42
53
|
self._entities_cache[entity_id] = entity_cls(entity_model)
|
|
43
54
|
return self._entities_cache[entity_id]
|
|
44
55
|
|
|
45
|
-
def perform_action(self, entity_id: str, action: EntityActions):
|
|
56
|
+
def perform_action(self, entity_id: str, action: EntityActions, *args, **kwargs):
|
|
46
57
|
"""
|
|
47
58
|
Instantiates an entity of the given ID and calls the given action on it.
|
|
48
59
|
"""
|
|
49
60
|
entity = self.get_entity(entity_id)
|
|
50
61
|
if entity.supports(action):
|
|
51
|
-
action_ctx = ActionContext(
|
|
52
|
-
|
|
62
|
+
action_ctx = ActionContext(
|
|
63
|
+
console=cc,
|
|
64
|
+
project_root=self.project_root(),
|
|
65
|
+
default_role=self._default_role,
|
|
66
|
+
default_warehouse=self.default_warehouse,
|
|
67
|
+
)
|
|
68
|
+
return entity.perform(action, action_ctx, *args, **kwargs)
|
|
53
69
|
else:
|
|
54
70
|
raise ValueError(f'This entity type does not support "{action.value}"')
|
|
55
71
|
|
|
@@ -42,7 +42,7 @@ class _ConnectionContext:
|
|
|
42
42
|
self._user: Optional[str] = None
|
|
43
43
|
self._password: Optional[str] = None
|
|
44
44
|
self._authenticator: Optional[str] = None
|
|
45
|
-
self.
|
|
45
|
+
self._private_key_file: Optional[str] = None
|
|
46
46
|
self._warehouse: Optional[str] = None
|
|
47
47
|
self._mfa_passcode: Optional[str] = None
|
|
48
48
|
self._enable_diag: Optional[bool] = False
|
|
@@ -125,11 +125,11 @@ class _ConnectionContext:
|
|
|
125
125
|
self._authenticator = value
|
|
126
126
|
|
|
127
127
|
@property
|
|
128
|
-
def
|
|
129
|
-
return self.
|
|
128
|
+
def private_key_file(self) -> Optional[str]:
|
|
129
|
+
return self._private_key_file
|
|
130
130
|
|
|
131
|
-
def
|
|
132
|
-
self.
|
|
131
|
+
def set_private_key_file(self, value: Optional[str]):
|
|
132
|
+
self._private_key_file = value
|
|
133
133
|
|
|
134
134
|
@property
|
|
135
135
|
def warehouse(self) -> Optional[str]:
|
|
@@ -206,7 +206,7 @@ class _ConnectionContext:
|
|
|
206
206
|
"user": self.user,
|
|
207
207
|
"password": self.password,
|
|
208
208
|
"authenticator": self.authenticator,
|
|
209
|
-
"
|
|
209
|
+
"private_key_file": self.private_key_file,
|
|
210
210
|
"database": self.database,
|
|
211
211
|
"schema": self.schema,
|
|
212
212
|
"role": self.role,
|
|
@@ -134,11 +134,12 @@ AuthenticatorOption = typer.Option(
|
|
|
134
134
|
|
|
135
135
|
PrivateKeyPathOption = typer.Option(
|
|
136
136
|
None,
|
|
137
|
+
"--private-key-file",
|
|
137
138
|
"--private-key-path",
|
|
138
|
-
help="Snowflake private key path. Overrides the value specified for the connection.",
|
|
139
|
+
help="Snowflake private key file path. Overrides the value specified for the connection.",
|
|
139
140
|
hide_input=True,
|
|
140
141
|
callback=_callback(
|
|
141
|
-
lambda: get_cli_context_manager().connection_context.
|
|
142
|
+
lambda: get_cli_context_manager().connection_context.set_private_key_file
|
|
142
143
|
),
|
|
143
144
|
show_default=False,
|
|
144
145
|
rich_help_panel=_CONNECTION_SECTION,
|
|
@@ -440,18 +441,43 @@ def experimental_option(
|
|
|
440
441
|
)
|
|
441
442
|
|
|
442
443
|
|
|
444
|
+
class IdentifierType(click.ParamType):
|
|
445
|
+
name = "TEXT"
|
|
446
|
+
|
|
447
|
+
def convert(self, value, param, ctx):
|
|
448
|
+
return FQN.from_string(value)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class IdentifierStageType(click.ParamType):
|
|
452
|
+
name = "TEXT"
|
|
453
|
+
|
|
454
|
+
def convert(self, value, param, ctx):
|
|
455
|
+
return FQN.from_stage(value)
|
|
456
|
+
|
|
457
|
+
|
|
443
458
|
def identifier_argument(
|
|
444
|
-
sf_object: str,
|
|
459
|
+
sf_object: str,
|
|
460
|
+
example: str,
|
|
461
|
+
click_type: click.ParamType = IdentifierType(),
|
|
462
|
+
callback: Callable | None = None,
|
|
445
463
|
) -> typer.Argument:
|
|
446
464
|
return typer.Argument(
|
|
447
465
|
...,
|
|
448
466
|
help=f"Identifier of the {sf_object}. For example: {example}",
|
|
449
467
|
show_default=False,
|
|
450
|
-
click_type=
|
|
468
|
+
click_type=click_type,
|
|
451
469
|
callback=callback,
|
|
452
470
|
)
|
|
453
471
|
|
|
454
472
|
|
|
473
|
+
def identifier_stage_argument(
|
|
474
|
+
sf_object: str, example: str, callback: Callable | None = None
|
|
475
|
+
) -> typer.Argument:
|
|
476
|
+
return identifier_argument(
|
|
477
|
+
sf_object, example, click_type=IdentifierStageType(), callback=callback
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
455
481
|
def execution_identifier_argument(sf_object: str, example: str) -> typer.Argument:
|
|
456
482
|
return typer.Argument(
|
|
457
483
|
...,
|
|
@@ -506,7 +532,7 @@ def project_env_overrides_option():
|
|
|
506
532
|
return typer.Option(
|
|
507
533
|
[],
|
|
508
534
|
"--env",
|
|
509
|
-
help="String in format of key=value. Overrides variables from env section used for
|
|
535
|
+
help="String in format of key=value. Overrides variables from env section used for templates.",
|
|
510
536
|
callback=_callback(lambda: project_env_overrides_callback),
|
|
511
537
|
show_default=False,
|
|
512
538
|
)
|
|
@@ -530,10 +556,3 @@ def deprecated_flag_callback_enum(msg: str):
|
|
|
530
556
|
return value.value
|
|
531
557
|
|
|
532
558
|
return _warning_callback
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
class IdentifierType(click.ParamType):
|
|
536
|
-
name = "TEXT"
|
|
537
|
-
|
|
538
|
-
def convert(self, value, param, ctx):
|
|
539
|
-
return FQN.from_string(value)
|
|
@@ -20,6 +20,7 @@ from functools import wraps
|
|
|
20
20
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
21
21
|
|
|
22
22
|
import typer
|
|
23
|
+
from click import ClickException
|
|
23
24
|
from snowflake.cli.api.commands.decorators import (
|
|
24
25
|
global_options,
|
|
25
26
|
global_options_with_connection,
|
|
@@ -33,6 +34,7 @@ from snowflake.cli.api.commands.typer_pre_execute import run_pre_execute_command
|
|
|
33
34
|
from snowflake.cli.api.exceptions import CommandReturnTypeError
|
|
34
35
|
from snowflake.cli.api.output.types import CommandResult
|
|
35
36
|
from snowflake.cli.api.sanitizers import sanitize_for_terminal
|
|
37
|
+
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
36
38
|
|
|
37
39
|
log = logging.getLogger(__name__)
|
|
38
40
|
|
|
@@ -71,6 +73,7 @@ class SnowTyper(typer.Typer):
|
|
|
71
73
|
requires_global_options: bool = True,
|
|
72
74
|
requires_connection: bool = False,
|
|
73
75
|
is_enabled: Callable[[], bool] | None = None,
|
|
76
|
+
require_warehouse: bool = False,
|
|
74
77
|
**kwargs,
|
|
75
78
|
):
|
|
76
79
|
"""
|
|
@@ -97,7 +100,7 @@ class SnowTyper(typer.Typer):
|
|
|
97
100
|
def command_callable_decorator(*args, **kw):
|
|
98
101
|
"""Wrapper around command callable. This is what happens at "runtime"."""
|
|
99
102
|
execution = ExecutionMetadata()
|
|
100
|
-
self.pre_execute(execution)
|
|
103
|
+
self.pre_execute(execution, require_warehouse=require_warehouse)
|
|
101
104
|
try:
|
|
102
105
|
result = command_callable(*args, **kw)
|
|
103
106
|
self.process_result(result)
|
|
@@ -116,7 +119,7 @@ class SnowTyper(typer.Typer):
|
|
|
116
119
|
return custom_command
|
|
117
120
|
|
|
118
121
|
@staticmethod
|
|
119
|
-
def pre_execute(execution: ExecutionMetadata):
|
|
122
|
+
def pre_execute(execution: ExecutionMetadata, require_warehouse: bool = False):
|
|
120
123
|
"""
|
|
121
124
|
Callback executed before running any command callable (after context execution).
|
|
122
125
|
Pay attention to make this method safe to use if performed operations are not necessary
|
|
@@ -127,6 +130,10 @@ class SnowTyper(typer.Typer):
|
|
|
127
130
|
log.debug("Executing command pre execution callback")
|
|
128
131
|
run_pre_execute_commands()
|
|
129
132
|
log_command_usage(execution)
|
|
133
|
+
if require_warehouse and not SqlExecutionMixin().session_has_warehouse():
|
|
134
|
+
raise ClickException(
|
|
135
|
+
"The command requires warehouse. No warehouse found in current connection."
|
|
136
|
+
)
|
|
130
137
|
|
|
131
138
|
@staticmethod
|
|
132
139
|
def process_result(result):
|
snowflake/cli/api/config.py
CHANGED
|
@@ -30,7 +30,10 @@ from snowflake.cli.api.exceptions import (
|
|
|
30
30
|
UnsupportedConfigSectionTypeError,
|
|
31
31
|
)
|
|
32
32
|
from snowflake.cli.api.secure_path import SecurePath
|
|
33
|
-
from snowflake.cli.api.secure_utils import
|
|
33
|
+
from snowflake.cli.api.secure_utils import (
|
|
34
|
+
file_permissions_are_strict,
|
|
35
|
+
windows_get_not_whitelisted_users_with_access,
|
|
36
|
+
)
|
|
34
37
|
from snowflake.cli.api.utils.types import try_cast_to_bool
|
|
35
38
|
from snowflake.connector.compat import IS_WINDOWS
|
|
36
39
|
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
@@ -77,7 +80,7 @@ class ConnectionConfig:
|
|
|
77
80
|
warehouse: Optional[str] = None
|
|
78
81
|
role: Optional[str] = None
|
|
79
82
|
authenticator: Optional[str] = None
|
|
80
|
-
|
|
83
|
+
private_key_file: Optional[str] = None
|
|
81
84
|
token_file_path: Optional[str] = None
|
|
82
85
|
|
|
83
86
|
_other_settings: dict = field(default_factory=lambda: {})
|
|
@@ -160,6 +163,18 @@ def _read_config_file():
|
|
|
160
163
|
message="Bad owner or permissions.*",
|
|
161
164
|
module="snowflake.connector.config_manager",
|
|
162
165
|
)
|
|
166
|
+
|
|
167
|
+
if not file_permissions_are_strict(CONFIG_MANAGER.file_path):
|
|
168
|
+
users = ", ".join(
|
|
169
|
+
windows_get_not_whitelisted_users_with_access(
|
|
170
|
+
CONFIG_MANAGER.file_path
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
warnings.warn(
|
|
174
|
+
f"Unauthorized users ({users}) have access to configuration file {CONFIG_MANAGER.file_path}.\n"
|
|
175
|
+
f'Run `icacls "{CONFIG_MANAGER.file_path}" /deny <USER_ID>:F` on those users to restrict permissions.'
|
|
176
|
+
)
|
|
177
|
+
|
|
163
178
|
try:
|
|
164
179
|
CONFIG_MANAGER.read_config()
|
|
165
180
|
except ConfigSourceError as exception:
|
|
@@ -322,8 +337,6 @@ def _dump_config(conf_file_cache: Dict):
|
|
|
322
337
|
|
|
323
338
|
|
|
324
339
|
def _check_default_config_files_permissions() -> None:
|
|
325
|
-
if IS_WINDOWS:
|
|
326
|
-
return
|
|
327
340
|
if CONNECTIONS_FILE.exists() and not file_permissions_are_strict(CONNECTIONS_FILE):
|
|
328
341
|
raise ConfigFileTooWidePermissionsError(CONNECTIONS_FILE)
|
|
329
342
|
if CONFIG_FILE.exists() and not file_permissions_are_strict(CONFIG_FILE):
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -77,3 +77,14 @@ VALID_SCOPES = ["database", "schema", "compute-pool"]
|
|
|
77
77
|
DEFAULT_SIZE_LIMIT_MB = 128
|
|
78
78
|
|
|
79
79
|
SF_REST_API_URL_PREFIX = "/api/v2"
|
|
80
|
+
|
|
81
|
+
PROJECT_TEMPLATE_VARIABLE_OPENING = "<%"
|
|
82
|
+
PROJECT_TEMPLATE_VARIABLE_CLOSING = "%>"
|
|
83
|
+
|
|
84
|
+
INIT_TEMPLATE_VARIABLE_OPENING = "<!"
|
|
85
|
+
INIT_TEMPLATE_VARIABLE_CLOSING = "!>"
|
|
86
|
+
|
|
87
|
+
SNOWPARK_SHARED_MIXIN = "snowpark_shared"
|
|
88
|
+
|
|
89
|
+
DEFAULT_ENV_FILE = "environment.yml"
|
|
90
|
+
DEFAULT_PAGES_DIR = "pages"
|