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.
Files changed (48) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/printing.py +53 -13
  3. snowflake/cli/_app/snow_connector.py +1 -0
  4. snowflake/cli/_app/telemetry.py +2 -0
  5. snowflake/cli/_app/version_check.py +73 -6
  6. snowflake/cli/_plugins/cortex/commands.py +8 -3
  7. snowflake/cli/_plugins/cortex/manager.py +24 -20
  8. snowflake/cli/_plugins/dbt/commands.py +5 -2
  9. snowflake/cli/_plugins/git/manager.py +1 -11
  10. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -0
  11. snowflake/cli/_plugins/nativeapp/commands.py +3 -4
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +1 -2
  14. snowflake/cli/_plugins/nativeapp/version/commands.py +1 -2
  15. snowflake/cli/_plugins/project/commands.py +61 -10
  16. snowflake/cli/_plugins/project/manager.py +20 -1
  17. snowflake/cli/_plugins/snowpark/common.py +23 -11
  18. snowflake/cli/_plugins/snowpark/snowpark_entity.py +13 -5
  19. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +10 -2
  20. snowflake/cli/_plugins/spcs/image_registry/commands.py +2 -2
  21. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  22. snowflake/cli/_plugins/sql/commands.py +49 -1
  23. snowflake/cli/_plugins/sql/manager.py +14 -4
  24. snowflake/cli/_plugins/sql/repl.py +4 -0
  25. snowflake/cli/_plugins/stage/commands.py +30 -11
  26. snowflake/cli/_plugins/stage/diff.py +2 -0
  27. snowflake/cli/_plugins/stage/manager.py +79 -55
  28. snowflake/cli/_plugins/streamlit/streamlit_entity.py +17 -30
  29. snowflake/cli/api/artifacts/upload.py +1 -1
  30. snowflake/cli/api/cli_global_context.py +5 -14
  31. snowflake/cli/api/commands/decorators.py +7 -0
  32. snowflake/cli/api/commands/flags.py +12 -0
  33. snowflake/cli/api/commands/snow_typer.py +23 -2
  34. snowflake/cli/api/config.py +9 -5
  35. snowflake/cli/api/connections.py +1 -0
  36. snowflake/cli/api/entities/common.py +16 -13
  37. snowflake/cli/api/entities/utils.py +15 -9
  38. snowflake/cli/api/feature_flags.py +2 -5
  39. snowflake/cli/api/output/formats.py +6 -0
  40. snowflake/cli/api/output/types.py +48 -2
  41. snowflake/cli/api/rendering/sql_templates.py +67 -11
  42. snowflake/cli/api/stage_path.py +37 -5
  43. {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/METADATA +45 -12
  44. {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/RECORD +47 -48
  45. snowflake/cli/_plugins/project/feature_flags.py +0 -22
  46. {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/WHEEL +0 -0
  47. {snowflake_cli-3.9.0.dist-info → snowflake_cli-3.10.0.dist-info}/entry_points.txt +0 -0
  48. {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 ClickException, UsageError
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"@{path}{'/'if self.is_directory and not path.endswith('/') else ''}"
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 ClickException("Invalid stage path")
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
- def get_directory_from_file_path(self, file_path: str) -> List[str]:
199
- stage_path_length = len(Path(self.directory).parts)
200
- return list(Path(file_path).parts[1 + stage_path_length : -1])
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
- @staticmethod
251
- def build_path(stage_path: str) -> StagePath:
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 embedded stages
259
- if name.startswith("snow://") or 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"@{name}"
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("@") and not re.fullmatch(
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 ClickException(
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 ClickException(f"No files found on stage '{stage_path}'")
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 ClickException(f"No files matched pattern '{stage_path}'")
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 ClickException(
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 ClickException(
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
- stage_path=StageManager().stage_path_parts_from_str(stage_root),
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._get_identifier(schema, database)}"
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._get_identifier()};"
205
+ return f"DESCRIBE STREAMLIT {self._get_sql_identifier()};"
211
206
 
212
207
  def get_share_sql(self, to_role: str) -> str:
213
- return f"GRANT USAGE ON STREAMLIT {self._get_identifier()} TO ROLE {to_role};"
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._get_identifier()}();"
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
- if GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled():
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 "Checkout already exists" in str(
248
- e
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
- embeded_stage_name = (
255
- f"snow://streamlit/{self.model.fqn.using_connection(self._conn).identifier}"
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
- stage_path=StageManager().stage_path_parts_from_str(stage_root),
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
  )
@@ -30,7 +30,7 @@ def sync_artifacts_with_stage(
30
30
  bundle_map=bundle_map,
31
31
  prune=prune,
32
32
  recursive=True,
33
- stage_path=stage_path_parts,
33
+ stage_path_parts=stage_path_parts,
34
34
  print_diff=True,
35
35
  )
36
36
  project_paths.clean_up_output()
@@ -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, Optional
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 self._manager.output_format == OutputFormat.JSON
201
-
202
- @property
203
- def snow_api_root(
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=self.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:
@@ -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
- if conf_file_cache.get(CLI_SECTION) is None:
208
- conf_file_cache[CLI_SECTION] = _DEFAULT_CLI_CONFIG
209
- if conf_file_cache[CLI_SECTION].get(LOGS_SECTION) is None:
210
- conf_file_cache[CLI_SECTION][LOGS_SECTION] = _DEFAULT_LOGS_CONFIG
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
 
@@ -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