snowflake-cli 3.1.0__py3-none-any.whl → 3.2.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 (60) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
  3. snowflake/cli/_plugins/connection/commands.py +124 -109
  4. snowflake/cli/_plugins/connection/util.py +54 -9
  5. snowflake/cli/_plugins/cortex/manager.py +1 -1
  6. snowflake/cli/_plugins/git/manager.py +4 -4
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  9. snowflake/cli/_plugins/nativeapp/commands.py +10 -3
  10. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  11. snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
  13. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  14. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  15. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  16. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  17. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  19. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
  20. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
  21. snowflake/cli/_plugins/notebook/manager.py +2 -2
  22. snowflake/cli/_plugins/object/commands.py +10 -1
  23. snowflake/cli/_plugins/object/manager.py +13 -5
  24. snowflake/cli/_plugins/snowpark/common.py +3 -3
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
  26. snowflake/cli/_plugins/spcs/common.py +29 -0
  27. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  28. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  29. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  30. snowflake/cli/_plugins/spcs/services/commands.py +64 -13
  31. snowflake/cli/_plugins/spcs/services/manager.py +75 -15
  32. snowflake/cli/_plugins/sql/commands.py +9 -1
  33. snowflake/cli/_plugins/sql/manager.py +9 -4
  34. snowflake/cli/_plugins/stage/commands.py +20 -16
  35. snowflake/cli/_plugins/stage/diff.py +1 -1
  36. snowflake/cli/_plugins/stage/manager.py +140 -11
  37. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  38. snowflake/cli/_plugins/workspace/commands.py +6 -3
  39. snowflake/cli/api/cli_global_context.py +1 -0
  40. snowflake/cli/api/config.py +23 -5
  41. snowflake/cli/api/console/console.py +4 -19
  42. snowflake/cli/api/entities/utils.py +19 -32
  43. snowflake/cli/api/errno.py +2 -0
  44. snowflake/cli/api/exceptions.py +9 -0
  45. snowflake/cli/api/metrics.py +223 -7
  46. snowflake/cli/api/output/types.py +1 -1
  47. snowflake/cli/api/project/definition_conversion.py +179 -62
  48. snowflake/cli/api/rest_api.py +26 -4
  49. snowflake/cli/api/secure_utils.py +1 -1
  50. snowflake/cli/api/sql_execution.py +35 -22
  51. snowflake/cli/api/stage_path.py +5 -2
  52. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
  53. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
  54. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  55. snowflake/cli/_plugins/nativeapp/manager.py +0 -392
  56. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  57. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  58. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
  59. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  60. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,17 +17,21 @@ from __future__ import annotations
17
17
  import fnmatch
18
18
  import glob
19
19
  import logging
20
+ import os
20
21
  import re
22
+ import shutil
21
23
  import sys
22
24
  import time
25
+ from collections import deque
23
26
  from contextlib import nullcontext
24
27
  from dataclasses import dataclass
25
28
  from os import path
26
29
  from pathlib import Path
30
+ from tempfile import TemporaryDirectory
27
31
  from textwrap import dedent
28
- from typing import Dict, List, Optional, Union
32
+ from typing import Deque, Dict, Generator, List, Optional, Union
29
33
 
30
- from click import ClickException
34
+ from click import ClickException, UsageError
31
35
  from snowflake.cli._plugins.snowpark.package_utils import parse_requirements
32
36
  from snowflake.cli.api.commands.common import (
33
37
  OnErrorType,
@@ -260,7 +264,7 @@ class StageManager(SqlExecutionMixin):
260
264
  query = f"ls {stage_path}"
261
265
  if pattern is not None:
262
266
  query += f" pattern = '{pattern}'"
263
- return self._execute_query(query, cursor_class=DictCursor)
267
+ return self.execute_query(query, cursor_class=DictCursor)
264
268
 
265
269
  @staticmethod
266
270
  def _assure_is_existing_directory(path: Path) -> None:
@@ -275,7 +279,7 @@ class StageManager(SqlExecutionMixin):
275
279
  spath = self.build_path(stage_path)
276
280
  self._assure_is_existing_directory(dest_path)
277
281
  dest_directory = f"{dest_path}/"
278
- return self._execute_query(
282
+ return self.execute_query(
279
283
  f"get {spath.path_for_sql()} {self._to_uri(dest_directory)} parallel={parallel}"
280
284
  )
281
285
 
@@ -291,7 +295,7 @@ class StageManager(SqlExecutionMixin):
291
295
  )
292
296
  self._assure_is_existing_directory(local_dir)
293
297
 
294
- result = self._execute_query(
298
+ result = self.execute_query(
295
299
  f"get {file_path.path_for_sql()} {self._to_uri(f'{local_dir}/')} parallel={parallel}"
296
300
  )
297
301
  results.append(result)
@@ -306,6 +310,7 @@ class StageManager(SqlExecutionMixin):
306
310
  overwrite: bool = False,
307
311
  role: Optional[str] = None,
308
312
  auto_compress: bool = False,
313
+ use_dict_cursor: bool = False,
309
314
  ) -> SnowflakeCursor:
310
315
  """
311
316
  This method will take a file path from the user's system and put it into a Snowflake stage,
@@ -313,16 +318,140 @@ class StageManager(SqlExecutionMixin):
313
318
  If provided with a role, then temporarily use this role to perform the operation above,
314
319
  and switch back to the original role for the next commands to run.
315
320
  """
321
+ if "*" not in str(local_path):
322
+ local_path = (
323
+ os.path.join(local_path, "*")
324
+ if Path(local_path).is_dir()
325
+ else str(local_path)
326
+ )
316
327
  with self.use_role(role) if role else nullcontext():
317
328
  spath = self.build_path(stage_path)
318
329
  local_resolved_path = path_resolver(str(local_path))
319
330
  log.info("Uploading %s to %s", local_resolved_path, stage_path)
320
- cursor = self._execute_query(
331
+ cursor = self.execute_query(
321
332
  f"put {self._to_uri(local_resolved_path)} {spath.path_for_sql()} "
322
- f"auto_compress={str(auto_compress).lower()} parallel={parallel} overwrite={overwrite}"
333
+ f"auto_compress={str(auto_compress).lower()} parallel={parallel} overwrite={overwrite}",
334
+ cursor_class=DictCursor if use_dict_cursor else SnowflakeCursor,
323
335
  )
324
336
  return cursor
325
337
 
338
+ @staticmethod
339
+ 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
+
342
+ absolute_src = resolve_without_follow(source_file_or_dir)
343
+ dest_path = dest_dir / source_file_or_dir.relative_to(source_root)
344
+
345
+ if absolute_src.is_file():
346
+ try:
347
+ os.symlink(absolute_src, dest_path)
348
+ except OSError:
349
+ if not dest_path.parent.exists():
350
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
351
+ shutil.copyfile(absolute_src, dest_path)
352
+ else:
353
+ dest_path.mkdir(exist_ok=True, parents=True)
354
+
355
+ def put_recursive(
356
+ self,
357
+ local_path: Path,
358
+ stage_path: str,
359
+ parallel: int = 4,
360
+ overwrite: bool = False,
361
+ role: Optional[str] = None,
362
+ auto_compress: bool = False,
363
+ ) -> Generator[dict, None, None]:
364
+ if local_path.is_file():
365
+ raise UsageError("Cannot use recursive upload with a single file.")
366
+
367
+ if local_path.is_dir():
368
+ root = local_path
369
+ glob_pattern = str(local_path / "**/*")
370
+ else:
371
+ root = Path([p for p in local_path.parents if p.is_dir()][0])
372
+ glob_pattern = str(local_path)
373
+
374
+ with TemporaryDirectory() as tmp:
375
+ temp_dir_with_copy = Path(tmp)
376
+
377
+ # Create a symlink or copy the file to the temp directory
378
+ for file_or_dir in glob.iglob(glob_pattern, recursive=True):
379
+ self._symlink_or_copy(
380
+ source_root=root,
381
+ source_file_or_dir=Path(file_or_dir),
382
+ dest_dir=temp_dir_with_copy,
383
+ )
384
+
385
+ # Find the deepest directories, we will be iterating from bottom to top
386
+ deepest_dirs_list = self._find_deepest_directories(temp_dir_with_copy)
387
+
388
+ while deepest_dirs_list:
389
+ # Remove as visited
390
+ directory = deepest_dirs_list.pop(0)
391
+
392
+ # We reached root but there are still directories to process
393
+ if directory == temp_dir_with_copy and deepest_dirs_list:
394
+ continue
395
+
396
+ # Upload the directory content, at this moment the directory has only files
397
+ if list(directory.iterdir()):
398
+ destination = StagePath.from_stage_str(
399
+ stage_path
400
+ ) / directory.relative_to(temp_dir_with_copy)
401
+ results: list[dict] = self.put(
402
+ local_path=directory,
403
+ stage_path=destination,
404
+ parallel=parallel,
405
+ overwrite=overwrite,
406
+ role=role,
407
+ auto_compress=auto_compress,
408
+ use_dict_cursor=True,
409
+ ).fetchall()
410
+
411
+ # Rewrite results to have resolved paths for better UX
412
+ for item in results:
413
+ item["source"] = (directory / item["source"]).relative_to(
414
+ temp_dir_with_copy
415
+ )
416
+ item["target"] = str(destination / item["target"])
417
+ yield item
418
+
419
+ # We end if we reach the root directory
420
+ if directory == temp_dir_with_copy:
421
+ break
422
+
423
+ # Add parent directory to the list if it's not already there
424
+ if directory.parent not in deepest_dirs_list:
425
+ deepest_dirs_list.append(directory.parent)
426
+
427
+ # Remove the directory so the parent directory will contain only files
428
+ shutil.rmtree(directory)
429
+
430
+ @staticmethod
431
+ def _find_deepest_directories(root_directory: Path) -> list[Path]:
432
+ """
433
+ BFS to find the deepest directories. Build a tree of directories
434
+ structure and return leaves.
435
+ """
436
+ deepest_dirs: list[Path] = list()
437
+
438
+ queue: Deque[Path] = deque()
439
+ queue.append(root_directory)
440
+ while queue:
441
+ current_dir = queue.popleft()
442
+ # Sorted to have deterministic order
443
+ children_directories = sorted(
444
+ list(d for d in current_dir.iterdir() if d.is_dir())
445
+ )
446
+ if not children_directories and current_dir not in deepest_dirs:
447
+ deepest_dirs.append(current_dir)
448
+ else:
449
+ queue.extend([c for c in children_directories if c not in deepest_dirs])
450
+ deepest_dirs_list = sorted(
451
+ list(deepest_dirs), key=lambda d: len(d.parts), reverse=True
452
+ )
453
+ return deepest_dirs_list
454
+
326
455
  def copy_files(self, source_path: str, destination_path: str) -> SnowflakeCursor:
327
456
  source_stage_path = self.build_path(source_path)
328
457
  # We copy only into stage
@@ -339,7 +468,7 @@ class StageManager(SqlExecutionMixin):
339
468
  # Destination needs to end with /
340
469
  dest = destination_stage_path.absolute_path().rstrip("/") + "/"
341
470
  query = f"copy files into {dest} from {source_stage_path}"
342
- return self._execute_query(query)
471
+ return self.execute_query(query)
343
472
 
344
473
  def remove(
345
474
  self, stage_name: str, path: str, role: Optional[str] = None
@@ -352,7 +481,7 @@ class StageManager(SqlExecutionMixin):
352
481
  """
353
482
  with self.use_role(role) if role else nullcontext():
354
483
  stage_path = self.build_path(stage_name) / path
355
- return self._execute_query(f"remove {stage_path.path_for_sql()}")
484
+ return self.execute_query(f"remove {stage_path.path_for_sql()}")
356
485
 
357
486
  def create(
358
487
  self, fqn: FQN, comment: Optional[str] = None, temporary: bool = False
@@ -361,7 +490,7 @@ class StageManager(SqlExecutionMixin):
361
490
  query = f"create {temporary_str}stage if not exists {fqn.sql_identifier}"
362
491
  if comment:
363
492
  query += f" comment='{comment}'"
364
- return self._execute_query(query)
493
+ return self.execute_query(query)
365
494
 
366
495
  def iter_stage(self, stage_path: StagePath):
367
496
  for file in self.list_files(stage_path.absolute_path()).fetchall():
@@ -557,7 +686,7 @@ class StageManager(SqlExecutionMixin):
557
686
  query = f"execute immediate from {self.quote_stage_name(file_stage_path)}"
558
687
  if variables:
559
688
  query += variables
560
- self._execute_query(query)
689
+ self.execute_query(query)
561
690
  return StageManager._success_result(file=original_file)
562
691
  except ProgrammingError as e:
563
692
  StageManager._handle_execution_exception(on_error=on_error, exception=e)
@@ -45,10 +45,10 @@ log = logging.getLogger(__name__)
45
45
  class StreamlitManager(SqlExecutionMixin):
46
46
  def execute(self, app_name: FQN):
47
47
  query = f"EXECUTE STREAMLIT {app_name.sql_identifier}()"
48
- return self._execute_query(query=query)
48
+ return self.execute_query(query=query)
49
49
 
50
50
  def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
51
- return self._execute_query(
51
+ return self.execute_query(
52
52
  f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
53
53
  )
54
54
 
@@ -118,7 +118,7 @@ class StreamlitManager(SqlExecutionMixin):
118
118
  if streamlit.secrets:
119
119
  query.append(streamlit.get_secrets_sql())
120
120
 
121
- self._execute_query("\n".join(query))
121
+ self.execute_query("\n".join(query))
122
122
 
123
123
  def deploy(self, streamlit: StreamlitEntityModel, replace: bool = False):
124
124
  streamlit_id = streamlit.fqn.using_connection(self._conn)
@@ -152,11 +152,11 @@ class StreamlitManager(SqlExecutionMixin):
152
152
  )
153
153
  try:
154
154
  if use_versioned_stage:
155
- self._execute_query(
155
+ self.execute_query(
156
156
  f"ALTER STREAMLIT {streamlit_id.identifier} ADD LIVE VERSION FROM LAST"
157
157
  )
158
158
  elif not FeatureFlag.ENABLE_STREAMLIT_NO_CHECKOUTS.is_enabled():
159
- self._execute_query(
159
+ self.execute_query(
160
160
  f"ALTER streamlit {streamlit_id.identifier} CHECKOUT"
161
161
  )
162
162
  except ProgrammingError as e:
@@ -22,7 +22,6 @@ from typing import List, Optional
22
22
 
23
23
  import typer
24
24
  import yaml
25
- from click import MissingParameter
26
25
  from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
27
26
  from snowflake.cli._plugins.nativeapp.common_flags import (
28
27
  ForceOption,
@@ -263,6 +262,11 @@ def version_create(
263
262
  help=f"""The patch number you want to create for an existing version.
264
263
  Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""",
265
264
  ),
265
+ label: Optional[str] = typer.Option(
266
+ None,
267
+ "--label",
268
+ help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
269
+ ),
266
270
  skip_git_check: Optional[bool] = typer.Option(
267
271
  False,
268
272
  "--skip-git-check",
@@ -274,8 +278,6 @@ def version_create(
274
278
  **options,
275
279
  ):
276
280
  """Creates a new version for the specified entity."""
277
- if version is None and patch is not None:
278
- raise MissingParameter("Cannot provide a patch without version!")
279
281
 
280
282
  cli_context = get_cli_context()
281
283
  ws = WorkspaceManager(
@@ -287,6 +289,7 @@ def version_create(
287
289
  EntityActions.VERSION_CREATE,
288
290
  version=version,
289
291
  patch=patch,
292
+ label=label,
290
293
  skip_git_check=skip_git_check,
291
294
  interactive=interactive,
292
295
  force=force,
@@ -76,6 +76,7 @@ class _CliGlobalContextManager:
76
76
  self,
77
77
  connection_context=self.connection_context.clone(),
78
78
  project_env_overrides_args=self.project_env_overrides_args.copy(),
79
+ metrics=self.metrics.clone(),
79
80
  )
80
81
 
81
82
  def __setattr__(self, prop, val):
@@ -181,7 +181,7 @@ def _read_config_file():
181
181
  )
182
182
  warnings.warn(
183
183
  f"Unauthorized users ({users}) have access to configuration file {CONFIG_MANAGER.file_path}.\n"
184
- f'Run `icacls "{CONFIG_MANAGER.file_path}" /deny <USER_ID>:F` on those users to restrict permissions.'
184
+ f'Run `icacls "{CONFIG_MANAGER.file_path}" /remove:g <USER_ID>` on those users to restrict permissions.'
185
185
  )
186
186
 
187
187
  try:
@@ -340,9 +340,24 @@ def _get_envs_for_path(*path) -> dict:
340
340
  }
341
341
 
342
342
 
343
- def _dump_config(conf_file_cache: Dict):
343
+ def _dump_config(config_and_connections: Dict):
344
+ config_toml_dict = config_and_connections.copy()
345
+
346
+ if CONNECTIONS_FILE.exists():
347
+ # update connections in connections.toml
348
+ # it will add only connections (maybe updated) which were originally read from connections.toml
349
+ # it won't add connections from config.toml
350
+ # because config manager doesn't have connections from config.toml if connections.toml exists
351
+ _update_connections_toml(config_and_connections.get("connections") or {})
352
+ # to config.toml save only connections from config.toml
353
+ connections_to_save_in_config_toml = _read_config_file_toml().get("connections")
354
+ if connections_to_save_in_config_toml:
355
+ config_toml_dict["connections"] = connections_to_save_in_config_toml
356
+ else:
357
+ config_toml_dict.pop("connections", None)
358
+
344
359
  with SecurePath(CONFIG_MANAGER.file_path).open("w+") as fh:
345
- dump(conf_file_cache, fh)
360
+ dump(config_toml_dict, fh)
346
361
 
347
362
 
348
363
  def _check_default_config_files_permissions() -> None:
@@ -373,9 +388,12 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
373
388
  return {k: _bool_or_unknown(v) for k, v in flags.items()}
374
389
 
375
390
 
391
+ def _read_config_file_toml() -> dict:
392
+ return tomlkit.loads(CONFIG_MANAGER.file_path.read_text()).unwrap()
393
+
394
+
376
395
  def _read_connections_toml() -> dict:
377
- with open(CONNECTIONS_FILE, "r") as f:
378
- return tomlkit.loads(f.read()).unwrap()
396
+ return tomlkit.loads(CONNECTIONS_FILE.read_text()).unwrap()
379
397
 
380
398
 
381
399
  def _update_connections_toml(connections: dict):
@@ -29,10 +29,6 @@ IMPORTANT_STYLE: Style = Style(bold=True, italic=True)
29
29
  INDENTATION_LEVEL: int = 2
30
30
 
31
31
 
32
- class CliConsoleNestingProhibitedError(RuntimeError):
33
- """CliConsole phase nesting not allowed."""
34
-
35
-
36
32
  class CliConsole(AbstractConsole):
37
33
  """An utility for displaying intermediate output.
38
34
 
@@ -70,28 +66,21 @@ class CliConsole(AbstractConsole):
70
66
  @contextmanager
71
67
  def phase(self, enter_message: str, exit_message: Optional[str] = None):
72
68
  """A context manager for organising steps into logical group."""
73
- if self.in_phase:
74
- raise CliConsoleNestingProhibitedError("Only one phase allowed at a time.")
75
- if self._extra_indent > 0:
76
- raise CliConsoleNestingProhibitedError(
77
- "Phase cannot be used in an indented block."
78
- )
79
-
80
69
  self._print(self._format_message(enter_message, Output.PHASE))
81
- self._in_phase = True
70
+ self._extra_indent += 1
82
71
 
83
72
  try:
84
73
  yield self.step
85
74
  finally:
86
- self._in_phase = False
75
+ self._extra_indent -= 1
87
76
  if exit_message:
88
77
  self._print(self._format_message(exit_message, Output.PHASE))
89
78
 
90
79
  @contextmanager
91
80
  def indented(self):
92
81
  """
93
- A context manager for temporarily indenting messages and warnings. Phases and steps cannot be used in indented blocks,
94
- but multiple indented blocks can be nested (use sparingly).
82
+ A context manager for temporarily indenting messages and warnings.
83
+ Multiple indented blocks can be nested (use sparingly).
95
84
  """
96
85
  self._extra_indent += 1
97
86
  try:
@@ -104,10 +93,6 @@ class CliConsole(AbstractConsole):
104
93
 
105
94
  If called within a phase, the output will be indented.
106
95
  """
107
- if self._extra_indent > 0:
108
- raise CliConsoleNestingProhibitedError(
109
- "Step cannot be used in an indented block."
110
- )
111
96
  text = self._format_message(message, Output.STEP)
112
97
  self._print(text)
113
98
 
@@ -12,6 +12,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
12
12
  InvalidTemplateInFileError,
13
13
  MissingScriptError,
14
14
  )
15
+ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
15
16
  from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
16
17
  from snowflake.cli._plugins.stage.diff import (
17
18
  DiffResult,
@@ -106,22 +107,15 @@ def sync_deploy_root_with_stage(
106
107
  A `DiffResult` instance describing the changes that were performed.
107
108
  """
108
109
 
109
- sql_executor = get_sql_executor()
110
+ sql_facade = get_snowflake_facade()
110
111
  # Does a stage already exist within the application package, or we need to create one?
111
112
  # Using "if not exists" should take care of either case.
112
113
  console.step(
113
114
  f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
114
115
  )
115
- with sql_executor.use_role(role):
116
- sql_executor.execute_query(
117
- f"create schema if not exists {package_name}.{stage_schema}"
118
- )
119
- sql_executor.execute_query(
120
- f"""
121
- create stage if not exists {stage_fqn}
122
- encryption = (TYPE = 'SNOWFLAKE_SSE')
123
- DIRECTORY = (ENABLE = TRUE)"""
124
- )
116
+ if not sql_facade.stage_exists(stage_fqn):
117
+ sql_facade.create_schema(stage_schema, database=package_name)
118
+ sql_facade.create_stage(stage_fqn)
125
119
 
126
120
  # Perform a diff operation and display results to the user for informational purposes
127
121
  if print_diff:
@@ -195,36 +189,24 @@ def sync_deploy_root_with_stage(
195
189
  return diff
196
190
 
197
191
 
198
- def _execute_sql_script(
199
- script_content: str,
200
- database_name: Optional[str] = None,
201
- ) -> None:
202
- """
203
- Executing the provided SQL script content.
204
- This assumes that a relevant warehouse is already active.
205
- If database_name is passed in, it will be used first.
206
- """
207
- try:
208
- sql_executor = get_sql_executor()
209
- if database_name is not None:
210
- sql_executor.execute_query(f"use database {database_name}")
211
- sql_executor.execute_queries(script_content)
212
- except ProgrammingError as err:
213
- generic_sql_error_handler(err)
214
-
215
-
216
192
  def execute_post_deploy_hooks(
217
193
  console: AbstractConsole,
218
194
  project_root: Path,
219
195
  post_deploy_hooks: Optional[List[PostDeployHook]],
220
196
  deployed_object_type: str,
197
+ role_name: str,
221
198
  database_name: str,
199
+ warehouse_name: str,
222
200
  ) -> None:
223
201
  """
224
202
  Executes post-deploy hooks for the given object type.
225
203
  While executing SQL post deploy hooks, it first switches to the database provided in the input.
226
204
  All post deploy scripts templates will first be expanded using the global template context.
227
205
  """
206
+ get_cli_context().metrics.set_counter_default(
207
+ CLICounterField.POST_DEPLOY_SCRIPTS, 0
208
+ )
209
+
228
210
  if not post_deploy_hooks:
229
211
  return
230
212
 
@@ -248,11 +230,16 @@ def execute_post_deploy_hooks(
248
230
  sql_scripts_paths,
249
231
  )
250
232
 
233
+ sql_facade = get_snowflake_facade()
234
+
251
235
  for index, sql_script_path in enumerate(display_paths):
252
236
  console.step(f"Executing SQL script: {sql_script_path}")
253
- _execute_sql_script(
254
- script_content=scripts_content_list[index],
255
- database_name=database_name,
237
+ sql_facade.execute_user_script(
238
+ queries=scripts_content_list[index],
239
+ script_name=sql_script_path,
240
+ role=role_name,
241
+ warehouse=warehouse_name,
242
+ database=database_name,
256
243
  )
257
244
 
258
245
 
@@ -26,3 +26,5 @@ ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046
26
26
  NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055
27
27
  APPLICATION_NO_LONGER_AVAILABLE = 93079
28
28
  APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
29
+ APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321
30
+ CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
@@ -190,6 +190,15 @@ class IncompatibleParametersError(UsageError):
190
190
  )
191
191
 
192
192
 
193
+ class UnmetParametersError(UsageError):
194
+ def __init__(self, options: list[str]):
195
+ options_with_quotes = [f"'{option}'" for option in options]
196
+ comma_separated_options = ", ".join(options_with_quotes[:-1])
197
+ super().__init__(
198
+ f"Parameters {comma_separated_options} and {options_with_quotes[-1]} must be used simultaneously."
199
+ )
200
+
201
+
193
202
  class NoWarehouseSelectedInSessionError(ClickException):
194
203
  def __init__(self, msg: str):
195
204
  super().__init__(