snowflake-cli 3.9.0__py3-none-any.whl → 3.10.0__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/printing.py +53 -13
- snowflake/cli/_app/snow_connector.py +1 -0
- snowflake/cli/_app/telemetry.py +2 -0
- snowflake/cli/_app/version_check.py +73 -6
- snowflake/cli/_plugins/cortex/commands.py +8 -3
- snowflake/cli/_plugins/cortex/manager.py +24 -20
- snowflake/cli/_plugins/dbt/commands.py +5 -2
- snowflake/cli/_plugins/git/manager.py +1 -11
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -0
- snowflake/cli/_plugins/nativeapp/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +1 -1
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +1 -2
- snowflake/cli/_plugins/nativeapp/version/commands.py +1 -2
- snowflake/cli/_plugins/project/commands.py +61 -10
- snowflake/cli/_plugins/project/manager.py +20 -1
- snowflake/cli/_plugins/snowpark/common.py +23 -11
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +13 -5
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +10 -2
- snowflake/cli/_plugins/spcs/image_registry/commands.py +2 -2
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/sql/commands.py +49 -1
- snowflake/cli/_plugins/sql/manager.py +14 -4
- snowflake/cli/_plugins/sql/repl.py +4 -0
- snowflake/cli/_plugins/stage/commands.py +30 -11
- snowflake/cli/_plugins/stage/diff.py +2 -0
- snowflake/cli/_plugins/stage/manager.py +79 -55
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +17 -30
- snowflake/cli/api/artifacts/upload.py +1 -1
- snowflake/cli/api/cli_global_context.py +5 -14
- snowflake/cli/api/commands/decorators.py +7 -0
- snowflake/cli/api/commands/flags.py +12 -0
- snowflake/cli/api/commands/snow_typer.py +23 -2
- snowflake/cli/api/config.py +9 -5
- snowflake/cli/api/connections.py +1 -0
- snowflake/cli/api/entities/common.py +16 -13
- snowflake/cli/api/entities/utils.py +15 -9
- snowflake/cli/api/feature_flags.py +2 -5
- snowflake/cli/api/output/formats.py +6 -0
- snowflake/cli/api/output/types.py +48 -2
- snowflake/cli/api/rendering/sql_templates.py +67 -11
- snowflake/cli/api/stage_path.py +37 -5
- {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/METADATA +45 -12
- {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/RECORD +47 -48
- snowflake/cli/_plugins/project/feature_flags.py +0 -22
- {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -239,6 +239,7 @@ def sync_local_diff_with_stage(
|
|
|
239
239
|
deploy_root_path: Path,
|
|
240
240
|
diff_result: DiffResult,
|
|
241
241
|
stage_full_path: str,
|
|
242
|
+
force_overwrite: bool = False,
|
|
242
243
|
):
|
|
243
244
|
"""
|
|
244
245
|
Syncs a given local directory's contents with a Snowflake stage, including removing old files, and re-uploading modified and new files.
|
|
@@ -267,6 +268,7 @@ def sync_local_diff_with_stage(
|
|
|
267
268
|
deploy_root_path=deploy_root_path,
|
|
268
269
|
stage_paths=diff_result.only_local,
|
|
269
270
|
role=role,
|
|
271
|
+
overwrite=force_overwrite,
|
|
270
272
|
)
|
|
271
273
|
except Exception as err:
|
|
272
274
|
# Could be ProgrammingError or IntegrityError from SnowflakeCursor
|
|
@@ -32,7 +32,7 @@ from tempfile import TemporaryDirectory
|
|
|
32
32
|
from textwrap import dedent
|
|
33
33
|
from typing import Deque, Dict, Generator, List, Optional, Union
|
|
34
34
|
|
|
35
|
-
from click import
|
|
35
|
+
from click import UsageError
|
|
36
36
|
from snowflake.cli._plugins.snowpark.package_utils import parse_requirements
|
|
37
37
|
from snowflake.cli.api.commands.common import (
|
|
38
38
|
OnErrorType,
|
|
@@ -41,6 +41,7 @@ from snowflake.cli.api.commands.common import (
|
|
|
41
41
|
from snowflake.cli.api.commands.utils import parse_key_value_variables
|
|
42
42
|
from snowflake.cli.api.console import cli_console
|
|
43
43
|
from snowflake.cli.api.constants import PYTHON_3_12
|
|
44
|
+
from snowflake.cli.api.exceptions import CliError
|
|
44
45
|
from snowflake.cli.api.identifiers import FQN
|
|
45
46
|
from snowflake.cli.api.project.util import VALID_IDENTIFIER_REGEX, to_string_literal
|
|
46
47
|
from snowflake.cli.api.secure_path import SecurePath
|
|
@@ -58,7 +59,10 @@ log = logging.getLogger(__name__)
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
UNQUOTED_FILE_URI_REGEX = r"[\w/*?\-.=&{}$#[\]\"\\!@%^+:]+"
|
|
62
|
+
AT_PREFIX = "@"
|
|
61
63
|
USER_STAGE_PREFIX = "@~"
|
|
64
|
+
SNOW_PREFIX = "snow://"
|
|
65
|
+
|
|
62
66
|
EXECUTE_SUPPORTED_FILES_FORMATS = (
|
|
63
67
|
".sql",
|
|
64
68
|
".py",
|
|
@@ -68,6 +72,17 @@ EXECUTE_SUPPORTED_FILES_FORMATS = (
|
|
|
68
72
|
OMIT_FIRST = slice(1, None)
|
|
69
73
|
STAGE_PATH_REGEX = rf"(?P<prefix>(@|{re.escape('snow://')}))?(?:(?P<first_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?:(?P<second_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?P<name>{VALID_IDENTIFIER_REGEX})/?(?P<directory>([^/]*/?)*)?"
|
|
70
74
|
|
|
75
|
+
# Define supported VSTAGE resource types
|
|
76
|
+
VSTAGE_RESOURCE_TYPE_REGEX = r"[a-zA-Z0-9\-]+"
|
|
77
|
+
VSTAGE_PATH_REGEX = (
|
|
78
|
+
rf"(?P<prefix>{re.escape(SNOW_PREFIX)})"
|
|
79
|
+
rf"(?P<resource_type>{VSTAGE_RESOURCE_TYPE_REGEX})/"
|
|
80
|
+
rf"(?:(?P<first_qualifier>{VALID_IDENTIFIER_REGEX})\.)?"
|
|
81
|
+
rf"(?:(?P<second_qualifier>{VALID_IDENTIFIER_REGEX})\.)?"
|
|
82
|
+
rf"(?P<name>{VALID_IDENTIFIER_REGEX})/?"
|
|
83
|
+
rf"(?P<directory>([^/]*/?)*)?"
|
|
84
|
+
)
|
|
85
|
+
|
|
71
86
|
|
|
72
87
|
class InternalStageEncryptionType(Enum):
|
|
73
88
|
SNOWFLAKE_FULL = "SNOWFLAKE_FULL"
|
|
@@ -80,6 +95,7 @@ class StagePathParts:
|
|
|
80
95
|
stage: str
|
|
81
96
|
stage_name: str
|
|
82
97
|
is_directory: bool
|
|
98
|
+
is_vstage: bool = False
|
|
83
99
|
|
|
84
100
|
@classmethod
|
|
85
101
|
def get_directory(cls, stage_path: str) -> str:
|
|
@@ -97,15 +113,9 @@ class StagePathParts:
|
|
|
97
113
|
def schema(self) -> str | None:
|
|
98
114
|
raise NotImplementedError
|
|
99
115
|
|
|
100
|
-
def replace_stage_prefix(self, file_path: str) -> str:
|
|
101
|
-
raise NotImplementedError
|
|
102
|
-
|
|
103
116
|
def add_stage_prefix(self, file_path: str) -> str:
|
|
104
117
|
raise NotImplementedError
|
|
105
118
|
|
|
106
|
-
def get_directory_from_file_path(self, file_path: str) -> List[str]:
|
|
107
|
-
raise NotImplementedError
|
|
108
|
-
|
|
109
119
|
def get_full_stage_path(self, path: str):
|
|
110
120
|
if prefix := FQN.from_stage_path(self.stage).prefix:
|
|
111
121
|
return prefix + "." + path
|
|
@@ -113,7 +123,7 @@ class StagePathParts:
|
|
|
113
123
|
|
|
114
124
|
def get_standard_stage_path(self) -> str:
|
|
115
125
|
path = self.get_full_stage_path(self.path)
|
|
116
|
-
return f"
|
|
126
|
+
return f"{AT_PREFIX}{path}{'/'if self.is_directory and not path.endswith('/') else ''}"
|
|
117
127
|
|
|
118
128
|
def get_standard_stage_directory_path(self) -> str:
|
|
119
129
|
path = self.get_standard_stage_path()
|
|
@@ -121,17 +131,6 @@ class StagePathParts:
|
|
|
121
131
|
return path + "/"
|
|
122
132
|
return path
|
|
123
133
|
|
|
124
|
-
def strip_stage_prefix(self, path: str):
|
|
125
|
-
raise NotImplementedError
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _strip_standard_stage_prefix(path: str) -> str:
|
|
129
|
-
"""Removes '@' or 'snow://' prefix from given string"""
|
|
130
|
-
for prefix in ["@", "snow://"]:
|
|
131
|
-
if path.startswith(prefix):
|
|
132
|
-
path = path.removeprefix(prefix)
|
|
133
|
-
return path
|
|
134
|
-
|
|
135
134
|
|
|
136
135
|
@dataclass
|
|
137
136
|
class DefaultStagePathParts(StagePathParts):
|
|
@@ -149,10 +148,10 @@ class DefaultStagePathParts(StagePathParts):
|
|
|
149
148
|
def __init__(self, stage_path: str):
|
|
150
149
|
match = re.fullmatch(STAGE_PATH_REGEX, stage_path)
|
|
151
150
|
if match is None:
|
|
152
|
-
raise
|
|
151
|
+
raise CliError("Invalid stage path")
|
|
153
152
|
self.directory = match.group("directory")
|
|
154
153
|
self._schema = match.group("second_qualifier") or match.group("first_qualifier")
|
|
155
|
-
self._prefix = match.group("prefix") or
|
|
154
|
+
self._prefix = match.group("prefix") or AT_PREFIX
|
|
156
155
|
self.stage = stage_path.removesuffix(self.directory).rstrip("/")
|
|
157
156
|
|
|
158
157
|
stage_name = FQN.from_stage(self.stage).name
|
|
@@ -180,24 +179,47 @@ class DefaultStagePathParts(StagePathParts):
|
|
|
180
179
|
def schema(self) -> str | None:
|
|
181
180
|
return self._schema
|
|
182
181
|
|
|
183
|
-
def replace_stage_prefix(self, file_path: str) -> str:
|
|
184
|
-
file_path = _strip_standard_stage_prefix(file_path)
|
|
185
|
-
file_path_without_prefix = Path(file_path).parts[OMIT_FIRST]
|
|
186
|
-
return f"{self.stage}/{'/'.join(file_path_without_prefix)}"
|
|
187
|
-
|
|
188
|
-
def strip_stage_prefix(self, file_path: str) -> str:
|
|
189
|
-
file_path = _strip_standard_stage_prefix(file_path)
|
|
190
|
-
if file_path.startswith(self.stage_name):
|
|
191
|
-
return file_path[len(self.stage_name) :]
|
|
192
|
-
return file_path
|
|
193
|
-
|
|
194
182
|
def add_stage_prefix(self, file_path: str) -> str:
|
|
195
183
|
stage = self.stage.rstrip("/")
|
|
196
184
|
return f"{stage}/{file_path.lstrip('/')}"
|
|
197
185
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class VStagePathParts(StagePathParts):
|
|
189
|
+
def __init__(self, stage_path: str):
|
|
190
|
+
match = re.fullmatch(VSTAGE_PATH_REGEX, stage_path)
|
|
191
|
+
if match is None or not match.group("resource_type") or not match.group("name"):
|
|
192
|
+
raise CliError(f"Invalid vstage path: {stage_path}.")
|
|
193
|
+
self.resource_type = match.group("resource_type")
|
|
194
|
+
self.directory = match.group("directory")
|
|
195
|
+
self._schema = match.group("second_qualifier") or match.group("first_qualifier")
|
|
196
|
+
self._prefix = match.group("prefix")
|
|
197
|
+
self.stage = stage_path.removesuffix(self.directory).rstrip("/")
|
|
198
|
+
self.stage_name = self.stage.removeprefix(self._prefix)
|
|
199
|
+
self.is_directory = True if stage_path.endswith("/") else False
|
|
200
|
+
self.is_vstage = True
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def path(self) -> str:
|
|
204
|
+
return f"{self._prefix}{self.stage_name.rstrip('/')}/{self.directory}".rstrip(
|
|
205
|
+
"/"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def full_path(self) -> str:
|
|
210
|
+
return f"{self._prefix}{self.stage_name.rstrip('/')}/{self.directory}".rstrip(
|
|
211
|
+
"/"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def schema(self) -> str | None:
|
|
216
|
+
return self._schema
|
|
217
|
+
|
|
218
|
+
def add_stage_prefix(self, file_path: str) -> str:
|
|
219
|
+
return self.full_path
|
|
220
|
+
|
|
221
|
+
def get_standard_stage_path(self) -> str:
|
|
222
|
+
return self.full_path
|
|
201
223
|
|
|
202
224
|
|
|
203
225
|
@dataclass
|
|
@@ -229,37 +251,29 @@ class UserStagePathParts(StagePathParts):
|
|
|
229
251
|
def full_path(self) -> str:
|
|
230
252
|
return f"{self.stage}/{self.directory}".rstrip("/")
|
|
231
253
|
|
|
232
|
-
def replace_stage_prefix(self, file_path: str) -> str:
|
|
233
|
-
if Path(file_path).parts[0] == self.stage_name:
|
|
234
|
-
return file_path
|
|
235
|
-
return f"{self.stage}/{file_path}"
|
|
236
|
-
|
|
237
254
|
def add_stage_prefix(self, file_path: str) -> str:
|
|
238
255
|
return f"{self.stage}/{file_path}"
|
|
239
256
|
|
|
240
|
-
def get_directory_from_file_path(self, file_path: str) -> List[str]:
|
|
241
|
-
stage_path_length = len(Path(self.directory).parts)
|
|
242
|
-
return list(Path(file_path).parts[stage_path_length:-1])
|
|
243
|
-
|
|
244
257
|
|
|
245
258
|
class StageManager(SqlExecutionMixin):
|
|
246
259
|
def __init__(self):
|
|
247
260
|
super().__init__()
|
|
248
261
|
self._python_exe_procedure = None
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
def build_path(self, stage_path: Union[str, StagePath]) -> StagePath:
|
|
264
|
+
if isinstance(stage_path, StagePath):
|
|
265
|
+
return stage_path
|
|
252
266
|
return StagePath.from_stage_str(stage_path)
|
|
253
267
|
|
|
254
268
|
@staticmethod
|
|
255
269
|
def get_standard_stage_prefix(name: str | FQN) -> str:
|
|
256
270
|
if isinstance(name, FQN):
|
|
257
271
|
name = name.identifier
|
|
258
|
-
# Handle
|
|
259
|
-
if name.startswith(
|
|
272
|
+
# Handle vstages
|
|
273
|
+
if name.startswith(SNOW_PREFIX) or name.startswith(AT_PREFIX):
|
|
260
274
|
return name
|
|
261
275
|
|
|
262
|
-
return f"
|
|
276
|
+
return f"{AT_PREFIX}{name}"
|
|
263
277
|
|
|
264
278
|
@staticmethod
|
|
265
279
|
def get_stage_from_path(path: str):
|
|
@@ -275,7 +289,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
275
289
|
return name # already quoted
|
|
276
290
|
|
|
277
291
|
standard_name = StageManager.get_standard_stage_prefix(name)
|
|
278
|
-
if standard_name.startswith(
|
|
292
|
+
if standard_name.startswith(AT_PREFIX) and not re.fullmatch(
|
|
279
293
|
r"@([\w./$])+", standard_name
|
|
280
294
|
):
|
|
281
295
|
return to_string_literal(standard_name)
|
|
@@ -503,7 +517,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
503
517
|
destination_stage_path = StagePath.from_stage_str(destination_path)
|
|
504
518
|
|
|
505
519
|
if destination_stage_path.is_user_stage():
|
|
506
|
-
raise
|
|
520
|
+
raise CliError(
|
|
507
521
|
"Destination path cannot be a user stage. Please provide a named stage."
|
|
508
522
|
)
|
|
509
523
|
|
|
@@ -534,11 +548,14 @@ class StageManager(SqlExecutionMixin):
|
|
|
534
548
|
comment: Optional[str] = None,
|
|
535
549
|
temporary: bool = False,
|
|
536
550
|
encryption: InternalStageEncryptionType | None = None,
|
|
551
|
+
enable_directory: bool = False,
|
|
537
552
|
) -> SnowflakeCursor:
|
|
538
553
|
temporary_str = "temporary " if temporary else ""
|
|
539
554
|
query = f"create {temporary_str}stage if not exists {fqn.sql_identifier}"
|
|
540
555
|
if encryption:
|
|
541
556
|
query += f" encryption = (type = '{encryption.value}')"
|
|
557
|
+
if enable_directory:
|
|
558
|
+
query += f" directory = (enable = true)"
|
|
542
559
|
if comment:
|
|
543
560
|
query += f" comment='{comment}'"
|
|
544
561
|
return self.execute_query(query)
|
|
@@ -572,7 +589,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
572
589
|
|
|
573
590
|
all_files_list = self._get_files_list_from_stage(stage_path.root_path())
|
|
574
591
|
if not all_files_list:
|
|
575
|
-
raise
|
|
592
|
+
raise CliError(f"No files found on stage '{stage_path}'")
|
|
576
593
|
|
|
577
594
|
all_files_with_stage_name_prefix = [
|
|
578
595
|
stage_path_parts.get_directory(file) for file in all_files_list
|
|
@@ -584,7 +601,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
584
601
|
)
|
|
585
602
|
|
|
586
603
|
if not filtered_file_list:
|
|
587
|
-
raise
|
|
604
|
+
raise CliError(f"No files matched pattern '{stage_path}'")
|
|
588
605
|
|
|
589
606
|
# sort filtered files in alphabetical order with directories at the end
|
|
590
607
|
sorted_file_path_list = sorted(
|
|
@@ -678,7 +695,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
678
695
|
if filtered_files:
|
|
679
696
|
return filtered_files
|
|
680
697
|
else:
|
|
681
|
-
raise
|
|
698
|
+
raise CliError(
|
|
682
699
|
f"Invalid file extension, only {', '.join(EXECUTE_SUPPORTED_FILES_FORMATS)} files are allowed."
|
|
683
700
|
)
|
|
684
701
|
# Filter with fnmatch if contains `*` or `?`
|
|
@@ -750,8 +767,15 @@ class StageManager(SqlExecutionMixin):
|
|
|
750
767
|
stage_path = StageManager.get_standard_stage_prefix(stage_path)
|
|
751
768
|
if stage_path.startswith(USER_STAGE_PREFIX):
|
|
752
769
|
return UserStagePathParts(stage_path)
|
|
770
|
+
elif stage_path.startswith(SNOW_PREFIX):
|
|
771
|
+
return VStagePathParts(stage_path)
|
|
753
772
|
return DefaultStagePathParts(stage_path)
|
|
754
773
|
|
|
774
|
+
def refresh(self, stage_name):
|
|
775
|
+
sql = f"ALTER STAGE {stage_name} REFRESH"
|
|
776
|
+
log.info("Refreshing stage %s", stage_name)
|
|
777
|
+
return self.execute_query(sql)
|
|
778
|
+
|
|
755
779
|
def _check_for_requirements_file(self, stage_path: StagePath) -> List[str]:
|
|
756
780
|
"""Looks for requirements.txt file on stage."""
|
|
757
781
|
current_dir = stage_path.parent if stage_path.is_file() else stage_path
|
|
@@ -800,7 +824,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
800
824
|
def _bootstrap_snowpark_execution_environment(self, stage_path: StagePath):
|
|
801
825
|
"""Prepares Snowpark session for executing Python code remotely."""
|
|
802
826
|
if sys.version_info >= PYTHON_3_12:
|
|
803
|
-
raise
|
|
827
|
+
raise CliError(
|
|
804
828
|
f"Executing Python files is not supported in Python >= 3.12. Current version: {sys.version}"
|
|
805
829
|
)
|
|
806
830
|
|
|
@@ -18,7 +18,7 @@ from snowflake.cli.api.identifiers import FQN
|
|
|
18
18
|
from snowflake.cli.api.project.project_paths import bundle_root
|
|
19
19
|
from snowflake.cli.api.project.schemas.entities.common import Identifier, PathMapping
|
|
20
20
|
from snowflake.connector import ProgrammingError
|
|
21
|
-
from snowflake.connector.cursor import SnowflakeCursor
|
|
21
|
+
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
|
|
22
22
|
|
|
23
23
|
log = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -102,7 +102,6 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
102
102
|
if (
|
|
103
103
|
experimental
|
|
104
104
|
or GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled()
|
|
105
|
-
or GlobalFeatureFlag.ENABLE_STREAMLIT_EMBEDDED_STAGE.is_enabled()
|
|
106
105
|
):
|
|
107
106
|
self._deploy_experimental(bundle_map=bundle_map, replace=replace)
|
|
108
107
|
else:
|
|
@@ -123,7 +122,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
123
122
|
bundle_map=bundle_map,
|
|
124
123
|
prune=prune,
|
|
125
124
|
recursive=True,
|
|
126
|
-
|
|
125
|
+
stage_path_parts=StageManager().stage_path_parts_from_str(stage_root),
|
|
127
126
|
print_diff=True,
|
|
128
127
|
)
|
|
129
128
|
|
|
@@ -136,7 +135,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
136
135
|
return self.perform(EntityActions.GET_URL, action_context, *args, **kwargs)
|
|
137
136
|
|
|
138
137
|
def describe(self) -> SnowflakeCursor:
|
|
139
|
-
return self._execute_query(self.get_describe_sql())
|
|
138
|
+
return self._execute_query(self.get_describe_sql(), cursor_class=DictCursor)
|
|
140
139
|
|
|
141
140
|
def action_share(
|
|
142
141
|
self, action_ctx: ActionContext, to_role: str, *args, **kwargs
|
|
@@ -146,13 +145,9 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
146
145
|
def get_add_live_version_sql(
|
|
147
146
|
self, schema: Optional[str] = None, database: Optional[str] = None
|
|
148
147
|
):
|
|
148
|
+
# this query unlike most others doesn't accept fqn wrapped in `IDENTIFIER('')`
|
|
149
149
|
return f"ALTER STREAMLIT {self._get_identifier(schema,database)} ADD LIVE VERSION FROM LAST;"
|
|
150
150
|
|
|
151
|
-
def get_checkout_sql(
|
|
152
|
-
self, schema: Optional[str] = None, database: Optional[str] = None
|
|
153
|
-
):
|
|
154
|
-
return f"ALTER STREAMLIT {self._get_identifier(schema,database)} CHECKOUT;"
|
|
155
|
-
|
|
156
151
|
def get_deploy_sql(
|
|
157
152
|
self,
|
|
158
153
|
if_not_exists: bool = False,
|
|
@@ -172,7 +167,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
172
167
|
else:
|
|
173
168
|
query = "CREATE STREAMLIT"
|
|
174
169
|
|
|
175
|
-
query += f" {self.
|
|
170
|
+
query += f" {self._get_sql_identifier(schema, database)}"
|
|
176
171
|
|
|
177
172
|
if from_stage_name:
|
|
178
173
|
query += f"\nROOT_LOCATION = '{from_stage_name}'"
|
|
@@ -207,13 +202,15 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
207
202
|
return query + ";"
|
|
208
203
|
|
|
209
204
|
def get_describe_sql(self) -> str:
|
|
210
|
-
return f"DESCRIBE STREAMLIT {self.
|
|
205
|
+
return f"DESCRIBE STREAMLIT {self._get_sql_identifier()};"
|
|
211
206
|
|
|
212
207
|
def get_share_sql(self, to_role: str) -> str:
|
|
213
|
-
return
|
|
208
|
+
return (
|
|
209
|
+
f"GRANT USAGE ON STREAMLIT {self._get_sql_identifier()} TO ROLE {to_role};"
|
|
210
|
+
)
|
|
214
211
|
|
|
215
212
|
def get_execute_sql(self):
|
|
216
|
-
return f"EXECUTE STREAMLIT {self.
|
|
213
|
+
return f"EXECUTE STREAMLIT {self._get_sql_identifier()}();"
|
|
217
214
|
|
|
218
215
|
def get_usage_grant_sql(self, app_role: str, schema: Optional[str] = None) -> str:
|
|
219
216
|
entity_id = self.entity_id
|
|
@@ -239,26 +236,15 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
239
236
|
)
|
|
240
237
|
)
|
|
241
238
|
try:
|
|
242
|
-
|
|
243
|
-
self._execute_query(self.get_add_live_version_sql())
|
|
244
|
-
elif not GlobalFeatureFlag.ENABLE_STREAMLIT_NO_CHECKOUTS.is_enabled():
|
|
245
|
-
self._execute_query(self.get_checkout_sql())
|
|
239
|
+
self._execute_query(self.get_add_live_version_sql())
|
|
246
240
|
except ProgrammingError as e:
|
|
247
|
-
if "
|
|
248
|
-
|
|
249
|
-
) or "There is already a live version" in str(e):
|
|
250
|
-
log.info("Checkout already exists, continuing")
|
|
241
|
+
if "There is already a live version" in str(e):
|
|
242
|
+
log.info("Live version already exists, continuing")
|
|
251
243
|
else:
|
|
252
244
|
raise
|
|
253
245
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
if GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled():
|
|
259
|
-
stage_root = f"{embeded_stage_name}/versions/live"
|
|
260
|
-
else:
|
|
261
|
-
stage_root = f"{embeded_stage_name}/default_checkout"
|
|
246
|
+
stage_root = self.describe().fetchone()["live_version_location_uri"]
|
|
247
|
+
stage_path_parts = StageManager().stage_path_parts_from_str(stage_root)
|
|
262
248
|
|
|
263
249
|
sync_deploy_root_with_stage(
|
|
264
250
|
console=self._workspace_ctx.console,
|
|
@@ -266,6 +252,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
266
252
|
bundle_map=bundle_map,
|
|
267
253
|
prune=prune,
|
|
268
254
|
recursive=True,
|
|
269
|
-
|
|
255
|
+
stage_path_parts=stage_path_parts,
|
|
270
256
|
print_diff=True,
|
|
257
|
+
force_overwrite=True, # files copied to streamlit vstage need to be overwritten
|
|
271
258
|
)
|
|
@@ -19,7 +19,7 @@ from contextvars import ContextVar
|
|
|
19
19
|
from dataclasses import dataclass, field, replace
|
|
20
20
|
from functools import wraps
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import TYPE_CHECKING, Iterator
|
|
22
|
+
from typing import TYPE_CHECKING, Iterator
|
|
23
23
|
|
|
24
24
|
from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
|
|
25
25
|
from snowflake.cli.api.exceptions import MissingConfigurationError
|
|
@@ -31,7 +31,6 @@ from snowflake.connector import SnowflakeConnection
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
32
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
33
33
|
from snowflake.cli.api.project.schemas.project_definition import ProjectDefinition
|
|
34
|
-
from snowflake.core import Root
|
|
35
34
|
|
|
36
35
|
_CONNECTION_CACHE = OpenConnectionCache()
|
|
37
36
|
|
|
@@ -197,18 +196,10 @@ class _CliGlobalContextAccess:
|
|
|
197
196
|
@property
|
|
198
197
|
def _should_force_mute_intermediate_output(self) -> bool:
|
|
199
198
|
"""Computes whether cli_console output should be muted."""
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
self,
|
|
205
|
-
) -> Optional[Root]:
|
|
206
|
-
from snowflake.core import Root
|
|
207
|
-
|
|
208
|
-
if self.connection:
|
|
209
|
-
return Root(self.connection)
|
|
210
|
-
else:
|
|
211
|
-
return None
|
|
199
|
+
return (
|
|
200
|
+
self._manager.output_format.is_json
|
|
201
|
+
or self._manager.output_format == OutputFormat.CSV
|
|
202
|
+
)
|
|
212
203
|
|
|
213
204
|
@property
|
|
214
205
|
def enhanced_exit_codes(self) -> bool:
|
|
@@ -53,6 +53,7 @@ from snowflake.cli.api.commands.flags import (
|
|
|
53
53
|
SilentOption,
|
|
54
54
|
TemporaryConnectionOption,
|
|
55
55
|
TokenFilePathOption,
|
|
56
|
+
TokenOption,
|
|
56
57
|
UserOption,
|
|
57
58
|
VerboseOption,
|
|
58
59
|
WarehouseOption,
|
|
@@ -279,6 +280,12 @@ GLOBAL_CONNECTION_OPTIONS = [
|
|
|
279
280
|
annotation=Optional[str],
|
|
280
281
|
default=MasterTokenOption,
|
|
281
282
|
),
|
|
283
|
+
inspect.Parameter(
|
|
284
|
+
"token",
|
|
285
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
286
|
+
annotation=Optional[str],
|
|
287
|
+
default=TokenOption,
|
|
288
|
+
),
|
|
282
289
|
inspect.Parameter(
|
|
283
290
|
"token_file_path",
|
|
284
291
|
inspect.Parameter.KEYWORD_ONLY,
|
|
@@ -212,6 +212,16 @@ MasterTokenOption = typer.Option(
|
|
|
212
212
|
hidden=True,
|
|
213
213
|
)
|
|
214
214
|
|
|
215
|
+
|
|
216
|
+
TokenOption = typer.Option(
|
|
217
|
+
None,
|
|
218
|
+
"--token",
|
|
219
|
+
help="OAuth token to use when connecting to Snowflake.",
|
|
220
|
+
callback=_connection_callback("token"),
|
|
221
|
+
show_default=False,
|
|
222
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
223
|
+
)
|
|
224
|
+
|
|
215
225
|
TokenFilePathOption = typer.Option(
|
|
216
226
|
None,
|
|
217
227
|
"--token-file-path",
|
|
@@ -274,6 +284,7 @@ MfaPasscodeOption = typer.Option(
|
|
|
274
284
|
rich_help_panel=_CONNECTION_SECTION,
|
|
275
285
|
)
|
|
276
286
|
|
|
287
|
+
|
|
277
288
|
EnableDiagOption = typer.Option(
|
|
278
289
|
False,
|
|
279
290
|
"--enable-diag",
|
|
@@ -435,6 +446,7 @@ OutputFormatOption = typer.Option(
|
|
|
435
446
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
436
447
|
)
|
|
437
448
|
|
|
449
|
+
|
|
438
450
|
SilentOption = typer.Option(
|
|
439
451
|
False,
|
|
440
452
|
"--silent",
|
|
@@ -46,6 +46,8 @@ from typer.core import TyperGroup
|
|
|
46
46
|
|
|
47
47
|
log = logging.getLogger(__name__)
|
|
48
48
|
|
|
49
|
+
PREVIEW_PREFIX = ""
|
|
50
|
+
|
|
49
51
|
|
|
50
52
|
class SortedTyperGroup(TyperGroup):
|
|
51
53
|
def list_commands(self, ctx: click.Context) -> List[str]:
|
|
@@ -92,6 +94,7 @@ class SnowTyper(typer.Typer):
|
|
|
92
94
|
requires_connection: bool = False,
|
|
93
95
|
is_enabled: Callable[[], bool] | None = None,
|
|
94
96
|
require_warehouse: bool = False,
|
|
97
|
+
preview: bool = False,
|
|
95
98
|
**kwargs,
|
|
96
99
|
):
|
|
97
100
|
"""
|
|
@@ -106,9 +109,18 @@ class SnowTyper(typer.Typer):
|
|
|
106
109
|
|
|
107
110
|
def custom_command(command_callable):
|
|
108
111
|
"""Custom command wrapper similar to Typer.command."""
|
|
109
|
-
# Sanitize doc string which is used to create help in terminal
|
|
110
112
|
command_callable.__doc__ = sanitize_for_terminal(command_callable.__doc__)
|
|
111
113
|
|
|
114
|
+
if preview and command_callable.__doc__:
|
|
115
|
+
if not command_callable.__doc__.strip().startswith(PREVIEW_PREFIX):
|
|
116
|
+
command_callable.__doc__ = (
|
|
117
|
+
f"{PREVIEW_PREFIX}{command_callable.__doc__.strip()}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if preview and "help" in kwargs and kwargs["help"]:
|
|
121
|
+
if not kwargs["help"].strip().startswith(PREVIEW_PREFIX):
|
|
122
|
+
kwargs["help"] = f"{PREVIEW_PREFIX}{kwargs['help'].strip()}"
|
|
123
|
+
|
|
112
124
|
if requires_connection:
|
|
113
125
|
command_callable = global_options_with_connection(command_callable)
|
|
114
126
|
elif requires_global_options:
|
|
@@ -228,6 +240,7 @@ class SnowTyperFactory:
|
|
|
228
240
|
short_help: Optional[str] = None,
|
|
229
241
|
is_hidden: Optional[Callable[[], bool]] = None,
|
|
230
242
|
deprecated: bool = False,
|
|
243
|
+
preview: bool = False,
|
|
231
244
|
subcommand_metavar: Optional[str] = None,
|
|
232
245
|
):
|
|
233
246
|
self.name = name
|
|
@@ -235,15 +248,21 @@ class SnowTyperFactory:
|
|
|
235
248
|
self.short_help = short_help
|
|
236
249
|
self.is_hidden = is_hidden
|
|
237
250
|
self.deprecated = deprecated
|
|
251
|
+
self.preview = preview
|
|
238
252
|
self.commands_to_register: List[SnowTyperCommandData] = []
|
|
239
253
|
self.subapps_to_register: List[SnowTyperFactory] = []
|
|
240
254
|
self.callbacks_to_register: List[Callable] = []
|
|
241
255
|
self.subcommand_metavar = subcommand_metavar
|
|
242
256
|
|
|
243
257
|
def create_instance(self) -> SnowTyper:
|
|
258
|
+
help_text = self.help
|
|
259
|
+
if self.preview and help_text:
|
|
260
|
+
if not help_text.strip().startswith(PREVIEW_PREFIX):
|
|
261
|
+
help_text = f"{PREVIEW_PREFIX}{help_text.strip()}"
|
|
262
|
+
|
|
244
263
|
app = SnowTyper(
|
|
245
264
|
name=self.name,
|
|
246
|
-
help=
|
|
265
|
+
help=help_text,
|
|
247
266
|
short_help=self.short_help,
|
|
248
267
|
hidden=self.is_hidden() if self.is_hidden else False,
|
|
249
268
|
deprecated=self.deprecated,
|
|
@@ -251,6 +270,8 @@ class SnowTyperFactory:
|
|
|
251
270
|
)
|
|
252
271
|
# register commands
|
|
253
272
|
for command in self.commands_to_register:
|
|
273
|
+
if self.preview and "preview" not in command.kwargs:
|
|
274
|
+
command.kwargs["preview"] = True
|
|
254
275
|
app.command(*command.args, **command.kwargs)(command.func)
|
|
255
276
|
# register callbacks
|
|
256
277
|
for callback in self.callbacks_to_register:
|
snowflake/cli/api/config.py
CHANGED
|
@@ -55,6 +55,7 @@ CONNECTIONS_SECTION = "connections"
|
|
|
55
55
|
CLI_SECTION = "cli"
|
|
56
56
|
LOGS_SECTION = "logs"
|
|
57
57
|
PLUGINS_SECTION = "plugins"
|
|
58
|
+
IGNORE_NEW_VERSION_WARNING_KEY = "ignore_new_version_warning"
|
|
58
59
|
|
|
59
60
|
LOGS_SECTION_PATH = [CLI_SECTION, LOGS_SECTION]
|
|
60
61
|
PLUGINS_SECTION_PATH = [CLI_SECTION, PLUGINS_SECTION]
|
|
@@ -204,10 +205,12 @@ def _read_config_file():
|
|
|
204
205
|
|
|
205
206
|
def _initialise_logs_section():
|
|
206
207
|
with _config_file() as conf_file_cache:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
conf_file_cache[CLI_SECTION][LOGS_SECTION] = _DEFAULT_LOGS_CONFIG
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _initialise_cli_section():
|
|
212
|
+
with _config_file() as conf_file_cache:
|
|
213
|
+
conf_file_cache[CLI_SECTION] = {IGNORE_NEW_VERSION_WARNING_KEY: False}
|
|
211
214
|
|
|
212
215
|
|
|
213
216
|
def set_config_value(path: List[str], value: Any) -> None:
|
|
@@ -297,7 +300,7 @@ def get_config_value(*path, key: str, default: Optional[Any] = Empty) -> Any:
|
|
|
297
300
|
return env_variable
|
|
298
301
|
try:
|
|
299
302
|
return get_config_section(*path)[key]
|
|
300
|
-
except (KeyError, NonExistentKey, MissingConfigOptionError):
|
|
303
|
+
except (KeyError, NonExistentKey, MissingConfigOptionError, ConfigSourceError):
|
|
301
304
|
if default is not Empty:
|
|
302
305
|
return default
|
|
303
306
|
raise
|
|
@@ -321,6 +324,7 @@ def _initialise_config(config_file: Path) -> None:
|
|
|
321
324
|
config_file = SecurePath(config_file)
|
|
322
325
|
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
323
326
|
config_file.touch()
|
|
327
|
+
_initialise_cli_section()
|
|
324
328
|
_initialise_logs_section()
|
|
325
329
|
log.info("Created Snowflake configuration file at %s", CONFIG_MANAGER.file_path)
|
|
326
330
|
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -48,6 +48,7 @@ class ConnectionContext:
|
|
|
48
48
|
private_key_file: Optional[str] = None
|
|
49
49
|
warehouse: Optional[str] = None
|
|
50
50
|
mfa_passcode: Optional[str] = None
|
|
51
|
+
token: Optional[str] = None
|
|
51
52
|
enable_diag: Optional[bool] = False
|
|
52
53
|
diag_log_path: Optional[Path] = None
|
|
53
54
|
diag_allowlist_path: Optional[Path] = None
|