snowflake-cli 3.2.2__py3-none-any.whl → 3.4.1__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/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +224 -192
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
- snowflake/cli/_app/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
- snowflake/cli/_plugins/nativeapp/commands.py +113 -21
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
- snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
- snowflake/cli/_plugins/notebook/commands.py +55 -2
- snowflake/cli/_plugins/notebook/exceptions.py +1 -1
- snowflake/cli/_plugins/notebook/manager.py +7 -5
- snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
- snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
- snowflake/cli/_plugins/notebook/types.py +3 -0
- snowflake/cli/_plugins/snowpark/commands.py +48 -30
- snowflake/cli/_plugins/snowpark/common.py +47 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
- snowflake/cli/_plugins/snowpark/zipper.py +33 -1
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +131 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +34 -13
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +10 -1
- snowflake/cli/_plugins/streamlit/manager.py +70 -22
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
- snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
- snowflake/cli/_plugins/workspace/commands.py +6 -5
- snowflake/cli/_plugins/workspace/manager.py +9 -5
- snowflake/cli/api/artifacts/__init__.py +13 -0
- snowflake/cli/api/artifacts/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +36 -2
- snowflake/cli/api/commands/flags.py +10 -4
- snowflake/cli/api/commands/utils.py +28 -2
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +10 -1
- snowflake/cli/api/entities/common.py +81 -14
- snowflake/cli/api/entities/resolver.py +160 -0
- snowflake/cli/api/entities/utils.py +65 -23
- snowflake/cli/api/errno.py +63 -3
- snowflake/cli/api/feature_flags.py +19 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +4 -4
- snowflake/cli/api/project/project_paths.py +28 -0
- snowflake/cli/api/project/schemas/entities/common.py +130 -1
- snowflake/cli/api/project/schemas/entities/entities.py +4 -0
- snowflake/cli/api/project/schemas/project_definition.py +54 -6
- snowflake/cli/api/project/schemas/updatable_model.py +2 -2
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- snowflake/cli/api/secure_path.py +6 -0
- snowflake/cli/api/sql_execution.py +5 -1
- snowflake/cli/api/stage_path.py +7 -2
- snowflake/cli/api/utils/graph.py +3 -0
- snowflake/cli/api/utils/path_utils.py +24 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,13 +19,14 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from pathlib import Path, PurePosixPath
|
|
20
20
|
from typing import Collection, Dict, List, Optional, Tuple
|
|
21
21
|
|
|
22
|
-
from snowflake.cli.
|
|
22
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
23
23
|
from snowflake.cli.api.exceptions import (
|
|
24
24
|
SnowflakeSQLExecutionError,
|
|
25
25
|
)
|
|
26
|
+
from snowflake.cli.api.project.util import unquote_identifier
|
|
26
27
|
from snowflake.connector.cursor import DictCursor
|
|
27
28
|
|
|
28
|
-
from .manager import StageManager
|
|
29
|
+
from .manager import StageManager, StagePathParts
|
|
29
30
|
from .md5 import UnknownMD5FormatError, file_matches_md5sum
|
|
30
31
|
|
|
31
32
|
log = logging.getLogger(__name__)
|
|
@@ -83,18 +84,31 @@ def enumerate_files(path: Path) -> List[Path]:
|
|
|
83
84
|
return paths
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def build_md5_map(list_stage_cursor: DictCursor) -> Dict[StagePathType, Optional[str]]:
|
|
87
|
+
def relative_to_stage_path(path: str, stage_path: StagePathParts) -> StagePathType:
|
|
88
|
+
"""
|
|
89
|
+
@param path: file path on the stage.
|
|
90
|
+
@param stage_path: stage path object.
|
|
91
|
+
@return: path of the file relative to the stage and subdirectory
|
|
92
92
|
"""
|
|
93
|
-
|
|
93
|
+
# path is returned from a SQL call so it's unquoted. Unquote stage_path identifiers to match.
|
|
94
|
+
# Stage is always returned in lower-case from ls SQL request
|
|
95
|
+
stage_name = unquote_identifier(stage_path.stage_name).lower()
|
|
96
|
+
stage_subdirectory = stage_path.directory
|
|
97
|
+
path_wo_stage_name = path.removeprefix(stage_name).lstrip("/")
|
|
98
|
+
relative_path = path_wo_stage_name.removeprefix(stage_subdirectory).lstrip("/")
|
|
99
|
+
return StagePathType(relative_path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_md5_map(
|
|
103
|
+
list_stage_cursor: DictCursor, stage_path: StagePathParts
|
|
104
|
+
) -> Dict[StagePathType, Optional[str]]:
|
|
94
105
|
"""
|
|
106
|
+
Returns a mapping of file paths to their md5sums. File paths are relative to the stage and subdirectory.
|
|
107
|
+
"""
|
|
108
|
+
all_files = list_stage_cursor.fetchall()
|
|
95
109
|
return {
|
|
96
|
-
|
|
97
|
-
for file in
|
|
110
|
+
relative_to_stage_path(file["name"], stage_path): file["md5"]
|
|
111
|
+
for file in all_files
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
|
|
@@ -115,50 +129,50 @@ def preserve_from_diff(
|
|
|
115
129
|
return preserved_diff
|
|
116
130
|
|
|
117
131
|
|
|
118
|
-
def compute_stage_diff(
|
|
119
|
-
local_root: Path,
|
|
120
|
-
stage_fqn: str,
|
|
121
|
-
) -> DiffResult:
|
|
132
|
+
def compute_stage_diff(local_root: Path, stage_path: StagePathParts) -> DiffResult:
|
|
122
133
|
"""
|
|
123
|
-
Diffs the files in
|
|
134
|
+
Diffs the files in the local_root with files in the stage path that is stage_path's full_path.
|
|
124
135
|
"""
|
|
125
136
|
stage_manager = StageManager()
|
|
126
137
|
local_files = enumerate_files(local_root)
|
|
127
|
-
|
|
138
|
+
remote_files = stage_manager.list_files(stage_path.full_path)
|
|
139
|
+
|
|
140
|
+
# Create a mapping from remote_file path to file's md5sum. Path is relative to stage_name/directory.
|
|
141
|
+
remote_md5 = build_md5_map(remote_files, stage_path)
|
|
128
142
|
|
|
129
143
|
result: DiffResult = DiffResult()
|
|
130
144
|
|
|
131
145
|
for local_file in local_files:
|
|
132
146
|
relpath = local_file.relative_to(local_root)
|
|
133
|
-
|
|
134
|
-
if
|
|
147
|
+
rel_stage_path = to_stage_path(relpath)
|
|
148
|
+
if rel_stage_path not in remote_md5:
|
|
135
149
|
# doesn't exist on the stage
|
|
136
|
-
result.only_local.append(
|
|
150
|
+
result.only_local.append(rel_stage_path)
|
|
137
151
|
else:
|
|
138
152
|
# N.B. file size on stage is not always accurate, so cannot fail fast
|
|
139
153
|
try:
|
|
140
|
-
if file_matches_md5sum(local_file, remote_md5[
|
|
154
|
+
if file_matches_md5sum(local_file, remote_md5[rel_stage_path]):
|
|
141
155
|
# We are assuming that we will not get accidental collisions here due to the
|
|
142
156
|
# large space of the md5sum (32 * 4 = 128 bits means 1-in-9-trillion chance)
|
|
143
157
|
# combined with the fact that the file name + path must also match elsewhere.
|
|
144
|
-
result.identical.append(
|
|
158
|
+
result.identical.append(rel_stage_path)
|
|
145
159
|
else:
|
|
146
160
|
# either the file has changed, or we can't tell if it has
|
|
147
|
-
result.different.append(
|
|
161
|
+
result.different.append(rel_stage_path)
|
|
148
162
|
except UnknownMD5FormatError:
|
|
149
163
|
log.warning(
|
|
150
164
|
"Could not compare md5 for %s, assuming file has changed",
|
|
151
165
|
local_file,
|
|
152
166
|
exc_info=True,
|
|
153
167
|
)
|
|
154
|
-
result.different.append(
|
|
168
|
+
result.different.append(rel_stage_path)
|
|
155
169
|
|
|
156
170
|
# mark this file as seen
|
|
157
|
-
del remote_md5[
|
|
171
|
+
del remote_md5[rel_stage_path]
|
|
158
172
|
|
|
159
173
|
# every entry here is a file we never saw locally
|
|
160
|
-
for
|
|
161
|
-
result.only_on_stage.append(
|
|
174
|
+
for rel_stage_path in remote_md5.keys():
|
|
175
|
+
result.only_on_stage.append(rel_stage_path)
|
|
162
176
|
|
|
163
177
|
return result
|
|
164
178
|
|
|
@@ -185,7 +199,7 @@ def to_local_path(stage_path: StagePathType) -> Path:
|
|
|
185
199
|
|
|
186
200
|
def delete_only_on_stage_files(
|
|
187
201
|
stage_manager: StageManager,
|
|
188
|
-
|
|
202
|
+
stage_root: str,
|
|
189
203
|
only_on_stage: List[StagePathType],
|
|
190
204
|
role: Optional[str] = None,
|
|
191
205
|
):
|
|
@@ -193,12 +207,12 @@ def delete_only_on_stage_files(
|
|
|
193
207
|
Deletes all files from a Snowflake stage according to the input list of filenames, using a custom role.
|
|
194
208
|
"""
|
|
195
209
|
for _stage_path in only_on_stage:
|
|
196
|
-
stage_manager.remove(stage_name=
|
|
210
|
+
stage_manager.remove(stage_name=stage_root, path=str(_stage_path), role=role)
|
|
197
211
|
|
|
198
212
|
|
|
199
213
|
def put_files_on_stage(
|
|
200
214
|
stage_manager: StageManager,
|
|
201
|
-
|
|
215
|
+
stage_root: str,
|
|
202
216
|
deploy_root_path: Path,
|
|
203
217
|
stage_paths: List[StagePathType],
|
|
204
218
|
role: Optional[str] = None,
|
|
@@ -210,7 +224,7 @@ def put_files_on_stage(
|
|
|
210
224
|
for _stage_path in stage_paths:
|
|
211
225
|
stage_sub_path = get_stage_subpath(_stage_path)
|
|
212
226
|
full_stage_path = (
|
|
213
|
-
f"{
|
|
227
|
+
f"{stage_root}/{stage_sub_path}" if stage_sub_path else stage_root
|
|
214
228
|
)
|
|
215
229
|
stage_manager.put(
|
|
216
230
|
local_path=deploy_root_path / to_local_path(_stage_path),
|
|
@@ -221,7 +235,10 @@ def put_files_on_stage(
|
|
|
221
235
|
|
|
222
236
|
|
|
223
237
|
def sync_local_diff_with_stage(
|
|
224
|
-
role: str | None,
|
|
238
|
+
role: str | None,
|
|
239
|
+
deploy_root_path: Path,
|
|
240
|
+
diff_result: DiffResult,
|
|
241
|
+
stage_full_path: str,
|
|
225
242
|
):
|
|
226
243
|
"""
|
|
227
244
|
Syncs a given local directory's contents with a Snowflake stage, including removing old files, and re-uploading modified and new files.
|
|
@@ -234,18 +251,22 @@ def sync_local_diff_with_stage(
|
|
|
234
251
|
|
|
235
252
|
try:
|
|
236
253
|
delete_only_on_stage_files(
|
|
237
|
-
stage_manager,
|
|
254
|
+
stage_manager, stage_full_path, diff_result.only_on_stage, role
|
|
238
255
|
)
|
|
239
256
|
put_files_on_stage(
|
|
240
|
-
stage_manager,
|
|
241
|
-
|
|
242
|
-
deploy_root_path,
|
|
243
|
-
diff_result.different,
|
|
244
|
-
role,
|
|
257
|
+
stage_manager=stage_manager,
|
|
258
|
+
stage_root=stage_full_path,
|
|
259
|
+
deploy_root_path=deploy_root_path,
|
|
260
|
+
stage_paths=diff_result.different,
|
|
261
|
+
role=role,
|
|
245
262
|
overwrite=True,
|
|
246
263
|
)
|
|
247
264
|
put_files_on_stage(
|
|
248
|
-
stage_manager,
|
|
265
|
+
stage_manager=stage_manager,
|
|
266
|
+
stage_root=stage_full_path,
|
|
267
|
+
deploy_root_path=deploy_root_path,
|
|
268
|
+
stage_paths=diff_result.only_local,
|
|
269
|
+
role=role,
|
|
249
270
|
)
|
|
250
271
|
except Exception as err:
|
|
251
272
|
# Could be ProgrammingError or IntegrityError from SnowflakeCursor
|
|
@@ -41,11 +41,11 @@ from snowflake.cli.api.commands.utils import parse_key_value_variables
|
|
|
41
41
|
from snowflake.cli.api.console import cli_console
|
|
42
42
|
from snowflake.cli.api.constants import PYTHON_3_12
|
|
43
43
|
from snowflake.cli.api.identifiers import FQN
|
|
44
|
-
from snowflake.cli.api.project.util import to_string_literal
|
|
44
|
+
from snowflake.cli.api.project.util import VALID_IDENTIFIER_REGEX, to_string_literal
|
|
45
45
|
from snowflake.cli.api.secure_path import SecurePath
|
|
46
46
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
47
47
|
from snowflake.cli.api.stage_path import StagePath
|
|
48
|
-
from snowflake.cli.api.utils.path_utils import path_resolver
|
|
48
|
+
from snowflake.cli.api.utils.path_utils import path_resolver, resolve_without_follow
|
|
49
49
|
from snowflake.connector import DictCursor, ProgrammingError
|
|
50
50
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
51
51
|
|
|
@@ -65,6 +65,7 @@ EXECUTE_SUPPORTED_FILES_FORMATS = (
|
|
|
65
65
|
|
|
66
66
|
# Replace magic numbers with constants
|
|
67
67
|
OMIT_FIRST = slice(1, None)
|
|
68
|
+
STAGE_PATH_REGEX = rf"(?P<prefix>@)?(?:(?P<first_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?:(?P<second_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?P<name>{VALID_IDENTIFIER_REGEX})/?(?P<directory>([^/]*/?)*)?"
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
@dataclass
|
|
@@ -86,6 +87,10 @@ class StagePathParts:
|
|
|
86
87
|
def full_path(self) -> str:
|
|
87
88
|
raise NotImplementedError
|
|
88
89
|
|
|
90
|
+
@property
|
|
91
|
+
def schema(self) -> str | None:
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
89
94
|
def replace_stage_prefix(self, file_path: str) -> str:
|
|
90
95
|
raise NotImplementedError
|
|
91
96
|
|
|
@@ -128,22 +133,38 @@ class DefaultStagePathParts(StagePathParts):
|
|
|
128
133
|
"""
|
|
129
134
|
|
|
130
135
|
def __init__(self, stage_path: str):
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
match = re.fullmatch(STAGE_PATH_REGEX, stage_path)
|
|
137
|
+
if match is None:
|
|
138
|
+
raise ClickException("Invalid stage path")
|
|
139
|
+
self.directory = match.group("directory")
|
|
140
|
+
self._schema = match.group("second_qualifier") or match.group("first_qualifier")
|
|
141
|
+
self.stage = stage_path.removesuffix(self.directory).rstrip("/")
|
|
142
|
+
|
|
143
|
+
stage_name = FQN.from_stage(self.stage).name
|
|
134
144
|
stage_name = (
|
|
135
145
|
stage_name[OMIT_FIRST] if stage_name.startswith("@") else stage_name
|
|
136
146
|
)
|
|
137
147
|
self.stage_name = stage_name
|
|
138
148
|
self.is_directory = True if stage_path.endswith("/") else False
|
|
139
149
|
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_fqn(
|
|
152
|
+
cls, stage_fqn: str, subdir: str | None = None
|
|
153
|
+
) -> DefaultStagePathParts:
|
|
154
|
+
full_path = f"{stage_fqn}/{subdir}" if subdir else stage_fqn
|
|
155
|
+
return cls(full_path)
|
|
156
|
+
|
|
140
157
|
@property
|
|
141
158
|
def path(self) -> str:
|
|
142
|
-
return f"{self.stage_name.rstrip('/')}/{self.directory}"
|
|
159
|
+
return f"{self.stage_name.rstrip('/')}/{self.directory}".rstrip("/")
|
|
143
160
|
|
|
144
161
|
@property
|
|
145
162
|
def full_path(self) -> str:
|
|
146
|
-
return f"{self.stage.rstrip('/')}/{self.directory}"
|
|
163
|
+
return f"{self.stage.rstrip('/')}/{self.directory}".rstrip("/")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def schema(self) -> str | None:
|
|
167
|
+
return self._schema
|
|
147
168
|
|
|
148
169
|
def replace_stage_prefix(self, file_path: str) -> str:
|
|
149
170
|
stage = Path(self.stage).parts[0]
|
|
@@ -193,7 +214,7 @@ class UserStagePathParts(StagePathParts):
|
|
|
193
214
|
|
|
194
215
|
@property
|
|
195
216
|
def full_path(self) -> str:
|
|
196
|
-
return f"{self.stage}/{self.directory}"
|
|
217
|
+
return f"{self.stage}/{self.directory}".rstrip("/")
|
|
197
218
|
|
|
198
219
|
def replace_stage_prefix(self, file_path: str) -> str:
|
|
199
220
|
if Path(file_path).parts[0] == self.stage_name:
|
|
@@ -337,7 +358,6 @@ class StageManager(SqlExecutionMixin):
|
|
|
337
358
|
|
|
338
359
|
@staticmethod
|
|
339
360
|
def _symlink_or_copy(source_root: Path, source_file_or_dir: Path, dest_dir: Path):
|
|
340
|
-
from snowflake.cli._plugins.nativeapp.artifacts import resolve_without_follow
|
|
341
361
|
|
|
342
362
|
absolute_src = resolve_without_follow(source_file_or_dir)
|
|
343
363
|
dest_path = dest_dir / source_file_or_dir.relative_to(source_root)
|
|
@@ -516,7 +536,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
516
536
|
stage_path_parts.get_standard_stage_path()
|
|
517
537
|
)
|
|
518
538
|
else:
|
|
519
|
-
stage_path_parts = self.
|
|
539
|
+
stage_path_parts = self.stage_path_parts_from_str(stage_path_str)
|
|
520
540
|
stage_path = self.build_path(stage_path_str)
|
|
521
541
|
|
|
522
542
|
all_files_list = self._get_files_list_from_stage(stage_path.root_path())
|
|
@@ -584,12 +604,12 @@ class StageManager(SqlExecutionMixin):
|
|
|
584
604
|
sm = StageManager()
|
|
585
605
|
|
|
586
606
|
# Rewrite stage paths to temporary stage paths. Git paths become stage paths
|
|
587
|
-
original_path_parts = self.
|
|
607
|
+
original_path_parts = self.stage_path_parts_from_str(stage_path) # noqa: SLF001
|
|
588
608
|
|
|
589
609
|
tmp_stage_name = f"snowflake_cli_tmp_stage_{int(time.time())}"
|
|
590
610
|
tmp_stage_fqn = FQN.from_stage(tmp_stage_name).using_connection(conn=self._conn)
|
|
591
611
|
tmp_stage = tmp_stage_fqn.identifier
|
|
592
|
-
stage_path_parts = sm.
|
|
612
|
+
stage_path_parts = sm.stage_path_parts_from_str( # noqa: SLF001
|
|
593
613
|
tmp_stage + "/" + original_path_parts.directory
|
|
594
614
|
)
|
|
595
615
|
|
|
@@ -693,7 +713,8 @@ class StageManager(SqlExecutionMixin):
|
|
|
693
713
|
return StageManager._error_result(file=original_file, msg=e.msg)
|
|
694
714
|
|
|
695
715
|
@staticmethod
|
|
696
|
-
def
|
|
716
|
+
def stage_path_parts_from_str(stage_path: str) -> StagePathParts:
|
|
717
|
+
"""Create StagePathParts object from stage path string."""
|
|
697
718
|
stage_path = StageManager.get_standard_stage_prefix(stage_path)
|
|
698
719
|
if stage_path.startswith(USER_STAGE_PREFIX):
|
|
699
720
|
return UserStagePathParts(stage_path)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
4
3
|
from snowflake.cli._plugins.stage.diff import (
|
|
5
4
|
DiffResult,
|
|
6
5
|
_to_diff_line,
|
|
7
6
|
_to_src_dest_pair,
|
|
8
7
|
)
|
|
8
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
9
9
|
from snowflake.cli.api.console import cli_console as cc
|
|
10
10
|
|
|
11
11
|
|
|
@@ -29,6 +29,9 @@ from snowflake.cli._plugins.streamlit.manager import StreamlitManager
|
|
|
29
29
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
30
30
|
StreamlitEntityModel,
|
|
31
31
|
)
|
|
32
|
+
from snowflake.cli._plugins.streamlit.streamlit_project_paths import (
|
|
33
|
+
StreamlitProjectPaths,
|
|
34
|
+
)
|
|
32
35
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
33
36
|
from snowflake.cli.api.commands.decorators import (
|
|
34
37
|
with_experimental_behaviour,
|
|
@@ -156,6 +159,8 @@ def streamlit_deploy(
|
|
|
156
159
|
entity_type="streamlit"
|
|
157
160
|
)
|
|
158
161
|
|
|
162
|
+
streamlit_project_paths = StreamlitProjectPaths(cli_context.project_root)
|
|
163
|
+
|
|
159
164
|
if not streamlits:
|
|
160
165
|
raise NoProjectDefinitionError(
|
|
161
166
|
project_type="streamlit", project_root=cli_context.project_root
|
|
@@ -174,7 +179,11 @@ def streamlit_deploy(
|
|
|
174
179
|
|
|
175
180
|
# Get first streamlit
|
|
176
181
|
streamlit: StreamlitEntityModel = streamlits[entity_id]
|
|
177
|
-
url = StreamlitManager().deploy(
|
|
182
|
+
url = StreamlitManager().deploy(
|
|
183
|
+
streamlit=streamlit,
|
|
184
|
+
streamlit_project_paths=streamlit_project_paths,
|
|
185
|
+
replace=replace,
|
|
186
|
+
)
|
|
178
187
|
|
|
179
188
|
if open_:
|
|
180
189
|
typer.launch(url)
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
-
from pathlib import
|
|
18
|
+
from pathlib import PurePosixPath
|
|
19
19
|
from typing import List, Optional
|
|
20
20
|
|
|
21
21
|
from click import ClickException
|
|
@@ -29,12 +29,18 @@ from snowflake.cli._plugins.stage.manager import StageManager
|
|
|
29
29
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
30
30
|
StreamlitEntityModel,
|
|
31
31
|
)
|
|
32
|
+
from snowflake.cli._plugins.streamlit.streamlit_project_paths import (
|
|
33
|
+
StreamlitProjectPaths,
|
|
34
|
+
)
|
|
35
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
36
|
+
from snowflake.cli.api.artifacts.utils import symlink_or_copy
|
|
32
37
|
from snowflake.cli.api.commands.experimental_behaviour import (
|
|
33
38
|
experimental_behaviour_enabled,
|
|
34
39
|
)
|
|
35
40
|
from snowflake.cli.api.console import cli_console
|
|
36
41
|
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
37
42
|
from snowflake.cli.api.identifiers import FQN
|
|
43
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
38
44
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
39
45
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
40
46
|
from snowflake.connector.errors import ProgrammingError
|
|
@@ -54,26 +60,45 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
54
60
|
|
|
55
61
|
def _put_streamlit_files(
|
|
56
62
|
self,
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
streamlit_project_paths: StreamlitProjectPaths,
|
|
64
|
+
stage_root: str,
|
|
65
|
+
artifacts: Optional[List[PathMapping]] = None,
|
|
59
66
|
):
|
|
60
|
-
cli_console.step(f"Deploying files to {
|
|
67
|
+
cli_console.step(f"Deploying files to {stage_root}")
|
|
61
68
|
if not artifacts:
|
|
62
69
|
return
|
|
63
70
|
stage_manager = StageManager()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
# We treat the bundle root as deploy root
|
|
72
|
+
bundle_map = BundleMap(
|
|
73
|
+
project_root=streamlit_project_paths.project_root,
|
|
74
|
+
deploy_root=streamlit_project_paths.bundle_root,
|
|
75
|
+
)
|
|
76
|
+
for artifact in artifacts:
|
|
77
|
+
bundle_map.add(PathMapping(src=str(artifact.src), dest=artifact.dest))
|
|
78
|
+
|
|
79
|
+
# Clean up bundle root
|
|
80
|
+
streamlit_project_paths.remove_up_bundle_root()
|
|
69
81
|
|
|
82
|
+
for (absolute_src, absolute_dest) in bundle_map.all_mappings(
|
|
83
|
+
absolute=True, expand_directories=True
|
|
84
|
+
):
|
|
85
|
+
if absolute_src.is_file():
|
|
86
|
+
# We treat the bundle/streamlit root as deploy root
|
|
87
|
+
symlink_or_copy(
|
|
88
|
+
absolute_src,
|
|
89
|
+
absolute_dest,
|
|
90
|
+
deploy_root=streamlit_project_paths.bundle_root,
|
|
91
|
+
)
|
|
92
|
+
# Temporary solution, will be replaced with diff
|
|
93
|
+
stage_path = (
|
|
94
|
+
PurePosixPath(absolute_dest)
|
|
95
|
+
.relative_to(streamlit_project_paths.bundle_root)
|
|
96
|
+
.parent
|
|
97
|
+
)
|
|
98
|
+
full_stage_path = f"{stage_root}/{stage_path}".rstrip("/")
|
|
70
99
|
stage_manager.put(
|
|
71
|
-
|
|
100
|
+
local_path=absolute_dest, stage_path=full_stage_path, overwrite=True
|
|
72
101
|
)
|
|
73
|
-
elif len(file.parts) > 1:
|
|
74
|
-
stage_manager.put(file, f"{root_location}/{file.parent}", 4, True)
|
|
75
|
-
else:
|
|
76
|
-
stage_manager.put(file, root_location, 4, True)
|
|
77
102
|
|
|
78
103
|
def _create_streamlit(
|
|
79
104
|
self,
|
|
@@ -104,8 +129,15 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
104
129
|
query.append(f"MAIN_FILE = '{streamlit.main_file}'")
|
|
105
130
|
if streamlit.imports:
|
|
106
131
|
query.append(streamlit.get_imports_sql())
|
|
107
|
-
|
|
132
|
+
|
|
133
|
+
if not streamlit.query_warehouse:
|
|
134
|
+
cli_console.warning(
|
|
135
|
+
"[Deprecation] In next major version we will remove default query_warehouse='streamlit'."
|
|
136
|
+
)
|
|
137
|
+
query.append(f"QUERY_WAREHOUSE = 'streamlit'")
|
|
138
|
+
else:
|
|
108
139
|
query.append(f"QUERY_WAREHOUSE = {streamlit.query_warehouse}")
|
|
140
|
+
|
|
109
141
|
if streamlit.title:
|
|
110
142
|
query.append(f"TITLE = '{streamlit.title}'")
|
|
111
143
|
|
|
@@ -120,7 +152,12 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
120
152
|
|
|
121
153
|
self.execute_query("\n".join(query))
|
|
122
154
|
|
|
123
|
-
def deploy(
|
|
155
|
+
def deploy(
|
|
156
|
+
self,
|
|
157
|
+
streamlit: StreamlitEntityModel,
|
|
158
|
+
streamlit_project_paths: StreamlitProjectPaths,
|
|
159
|
+
replace: bool = False,
|
|
160
|
+
):
|
|
124
161
|
streamlit_id = streamlit.fqn.using_connection(self._conn)
|
|
125
162
|
if (
|
|
126
163
|
ObjectManager().object_exists(object_type="streamlit", fqn=streamlit_id)
|
|
@@ -172,12 +209,13 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
172
209
|
embedded_stage_name = f"snow://streamlit/{stage_path}"
|
|
173
210
|
if use_versioned_stage:
|
|
174
211
|
# "LIVE" is the only supported version for now, but this may change later.
|
|
175
|
-
|
|
212
|
+
stage_root = f"{embedded_stage_name}/versions/live"
|
|
176
213
|
else:
|
|
177
|
-
|
|
214
|
+
stage_root = f"{embedded_stage_name}/default_checkout"
|
|
178
215
|
|
|
179
216
|
self._put_streamlit_files(
|
|
180
|
-
|
|
217
|
+
streamlit_project_paths,
|
|
218
|
+
stage_root,
|
|
181
219
|
streamlit.artifacts,
|
|
182
220
|
)
|
|
183
221
|
else:
|
|
@@ -194,21 +232,31 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
194
232
|
cli_console.step(f"Creating {stage_name} stage")
|
|
195
233
|
stage_manager.create(fqn=stage_name)
|
|
196
234
|
|
|
197
|
-
|
|
235
|
+
stage_root = stage_manager.get_standard_stage_prefix(
|
|
198
236
|
f"{stage_name}/{streamlit_name_for_root_location}"
|
|
199
237
|
)
|
|
200
238
|
|
|
201
|
-
self._put_streamlit_files(
|
|
239
|
+
self._put_streamlit_files(
|
|
240
|
+
streamlit_project_paths, stage_root, streamlit.artifacts
|
|
241
|
+
)
|
|
202
242
|
|
|
203
243
|
self._create_streamlit(
|
|
204
244
|
streamlit=streamlit,
|
|
205
245
|
replace=replace,
|
|
206
|
-
from_stage_name=
|
|
246
|
+
from_stage_name=stage_root,
|
|
207
247
|
experimental=False,
|
|
208
248
|
)
|
|
209
249
|
|
|
250
|
+
self.grant_privileges(streamlit)
|
|
251
|
+
|
|
210
252
|
return self.get_url(streamlit_name=streamlit_id)
|
|
211
253
|
|
|
254
|
+
def grant_privileges(self, entity_model: StreamlitEntityModel):
|
|
255
|
+
if not entity_model.grants:
|
|
256
|
+
return
|
|
257
|
+
for grant in entity_model.grants:
|
|
258
|
+
self.execute_query(grant.get_grant_sql(entity_model))
|
|
259
|
+
|
|
212
260
|
def get_url(self, streamlit_name: FQN) -> str:
|
|
213
261
|
try:
|
|
214
262
|
fqn = streamlit_name.using_connection(self._conn)
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from click import ClickException
|
|
5
|
+
from snowflake.cli._plugins.connection.util import make_snowsight_url
|
|
6
|
+
from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
|
|
7
|
+
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
|
|
1
8
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
2
9
|
StreamlitEntityModel,
|
|
3
10
|
)
|
|
11
|
+
from snowflake.cli._plugins.workspace.context import ActionContext
|
|
4
12
|
from snowflake.cli.api.entities.common import EntityBase
|
|
13
|
+
from snowflake.cli.api.project.project_paths import bundle_root
|
|
14
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
15
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
5
16
|
|
|
6
17
|
|
|
7
18
|
class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
@@ -9,4 +20,123 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
9
20
|
A Streamlit app.
|
|
10
21
|
"""
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
|
|
25
|
+
raise NotImplementedError("Streamlit entity is not implemented yet")
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def root(self):
|
|
30
|
+
return self._workspace_ctx.project_root
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def artifacts(self):
|
|
34
|
+
return self._entity_model.artifacts
|
|
35
|
+
|
|
36
|
+
def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
|
|
37
|
+
return self.bundle()
|
|
38
|
+
|
|
39
|
+
def action_deploy(self, action_ctx: ActionContext, *args, **kwargs):
|
|
40
|
+
# After adding bundle map- we should use it's mapping here
|
|
41
|
+
# To copy artifacts to destination on stage.
|
|
42
|
+
|
|
43
|
+
return self.deploy()
|
|
44
|
+
|
|
45
|
+
def action_drop(self, action_ctx: ActionContext, *args, **kwargs):
|
|
46
|
+
return self._execute_query(self.get_drop_sql())
|
|
47
|
+
|
|
48
|
+
def action_execute(
|
|
49
|
+
self, action_ctx: ActionContext, *args, **kwargs
|
|
50
|
+
) -> SnowflakeCursor:
|
|
51
|
+
return self._execute_query(self.get_execute_sql())
|
|
52
|
+
|
|
53
|
+
def action_get_url(
|
|
54
|
+
self, action_ctx: ActionContext, *args, **kwargs
|
|
55
|
+
): # maybe this should be a property
|
|
56
|
+
name = self._entity_model.fqn.using_connection(self._conn)
|
|
57
|
+
return make_snowsight_url(
|
|
58
|
+
self._conn, f"/#/streamlit-apps/{name.url_identifier}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def bundle(self, output_dir: Optional[Path] = None):
|
|
62
|
+
build_bundle(
|
|
63
|
+
self.root,
|
|
64
|
+
output_dir or bundle_root(self.root, "streamlit"),
|
|
65
|
+
[
|
|
66
|
+
PathMapping(
|
|
67
|
+
src=artifact.src, dest=artifact.dest, processors=artifact.processors
|
|
68
|
+
)
|
|
69
|
+
for artifact in self._entity_model.artifacts
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def deploy(self, *args, **kwargs):
|
|
74
|
+
return self._execute_query(self.get_deploy_sql())
|
|
75
|
+
|
|
76
|
+
def action_share(
|
|
77
|
+
self, action_ctx: ActionContext, to_role: str, *args, **kwargs
|
|
78
|
+
) -> SnowflakeCursor:
|
|
79
|
+
return self._execute_query(self.get_share_sql(to_role))
|
|
80
|
+
|
|
81
|
+
def get_deploy_sql(
|
|
82
|
+
self,
|
|
83
|
+
if_not_exists: bool = False,
|
|
84
|
+
replace: bool = False,
|
|
85
|
+
from_stage_name: Optional[str] = None,
|
|
86
|
+
artifacts_dir: Optional[Path] = None,
|
|
87
|
+
schema: Optional[str] = None,
|
|
88
|
+
*args,
|
|
89
|
+
**kwargs,
|
|
90
|
+
):
|
|
91
|
+
if replace and if_not_exists:
|
|
92
|
+
raise ClickException("Cannot specify both replace and if_not_exists")
|
|
93
|
+
|
|
94
|
+
if replace:
|
|
95
|
+
query = "CREATE OR REPLACE "
|
|
96
|
+
elif if_not_exists:
|
|
97
|
+
query = "CREATE IF NOT EXISTS "
|
|
98
|
+
else:
|
|
99
|
+
query = "CREATE "
|
|
100
|
+
|
|
101
|
+
schema_to_use = schema or self._entity_model.fqn.schema
|
|
102
|
+
query += f"STREAMLIT {self._entity_model.fqn.set_schema(schema_to_use).sql_identifier}"
|
|
103
|
+
|
|
104
|
+
if from_stage_name:
|
|
105
|
+
query += f"\nROOT_LOCATION = '{from_stage_name}'"
|
|
106
|
+
elif artifacts_dir:
|
|
107
|
+
query += f"\nFROM '{artifacts_dir}'"
|
|
108
|
+
|
|
109
|
+
query += f"\nMAIN_FILE = '{self._entity_model.main_file}'"
|
|
110
|
+
|
|
111
|
+
if self.model.imports:
|
|
112
|
+
query += "\n" + self.model.get_imports_sql()
|
|
113
|
+
|
|
114
|
+
if self.model.query_warehouse:
|
|
115
|
+
query += f"\nQUERY_WAREHOUSE = '{self.model.query_warehouse}'"
|
|
116
|
+
|
|
117
|
+
if self.model.title:
|
|
118
|
+
query += f"\nTITLE = '{self.model.title}'"
|
|
119
|
+
|
|
120
|
+
if self.model.comment:
|
|
121
|
+
query += f"\nCOMMENT = '{self.model.comment}'"
|
|
122
|
+
|
|
123
|
+
if self.model.external_access_integrations:
|
|
124
|
+
query += "\n" + self.model.get_external_access_integrations_sql()
|
|
125
|
+
|
|
126
|
+
if self.model.secrets:
|
|
127
|
+
query += "\n" + self.model.get_secrets_sql()
|
|
128
|
+
|
|
129
|
+
return query + ";"
|
|
130
|
+
|
|
131
|
+
def get_share_sql(self, to_role: str) -> str:
|
|
132
|
+
return f"GRANT USAGE ON STREAMLIT {self.model.fqn.sql_identifier} TO ROLE {to_role};"
|
|
133
|
+
|
|
134
|
+
def get_execute_sql(self):
|
|
135
|
+
return f"EXECUTE STREAMLIT {self._entity_model.fqn}();"
|
|
136
|
+
|
|
137
|
+
def get_usage_grant_sql(self, app_role: str, schema: Optional[str] = None) -> str:
|
|
138
|
+
entity_id = self.entity_id
|
|
139
|
+
streamlit_name = f"{schema}.{entity_id}" if schema else entity_id
|
|
140
|
+
return (
|
|
141
|
+
f"GRANT USAGE ON STREAMLIT {streamlit_name} TO APPLICATION ROLE {app_role};"
|
|
142
|
+
)
|