snowflake-cli 3.0.2__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 (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +152 -99
  7. snowflake/cli/_plugins/connection/util.py +54 -9
  8. snowflake/cli/_plugins/cortex/manager.py +1 -1
  9. snowflake/cli/_plugins/git/commands.py +6 -3
  10. snowflake/cli/_plugins/git/manager.py +9 -4
  11. snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  14. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  15. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  19. snowflake/cli/_plugins/nativeapp/commands.py +144 -188
  20. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
  23. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  24. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  25. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  26. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  27. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  28. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  29. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
  32. snowflake/cli/_plugins/notebook/manager.py +2 -2
  33. snowflake/cli/_plugins/object/commands.py +10 -1
  34. snowflake/cli/_plugins/object/manager.py +13 -5
  35. snowflake/cli/_plugins/snowpark/common.py +63 -21
  36. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
  37. snowflake/cli/_plugins/spcs/common.py +29 -0
  38. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  39. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  40. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  41. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  42. snowflake/cli/_plugins/spcs/services/commands.py +100 -17
  43. snowflake/cli/_plugins/spcs/services/manager.py +108 -16
  44. snowflake/cli/_plugins/sql/commands.py +9 -1
  45. snowflake/cli/_plugins/sql/manager.py +9 -4
  46. snowflake/cli/_plugins/stage/commands.py +28 -19
  47. snowflake/cli/_plugins/stage/diff.py +17 -17
  48. snowflake/cli/_plugins/stage/manager.py +304 -84
  49. snowflake/cli/_plugins/stage/md5.py +1 -1
  50. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  51. snowflake/cli/_plugins/workspace/commands.py +27 -4
  52. snowflake/cli/_plugins/workspace/context.py +38 -0
  53. snowflake/cli/_plugins/workspace/manager.py +23 -13
  54. snowflake/cli/api/cli_global_context.py +4 -3
  55. snowflake/cli/api/commands/flags.py +23 -7
  56. snowflake/cli/api/config.py +30 -9
  57. snowflake/cli/api/connections.py +12 -1
  58. snowflake/cli/api/console/console.py +4 -19
  59. snowflake/cli/api/entities/common.py +4 -2
  60. snowflake/cli/api/entities/utils.py +36 -69
  61. snowflake/cli/api/errno.py +2 -0
  62. snowflake/cli/api/exceptions.py +41 -0
  63. snowflake/cli/api/identifiers.py +8 -0
  64. snowflake/cli/api/metrics.py +223 -7
  65. snowflake/cli/api/output/types.py +1 -1
  66. snowflake/cli/api/project/definition_conversion.py +293 -77
  67. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  68. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  69. snowflake/cli/api/rest_api.py +26 -4
  70. snowflake/cli/api/secure_utils.py +1 -1
  71. snowflake/cli/api/sql_execution.py +40 -29
  72. snowflake/cli/api/stage_path.py +244 -0
  73. snowflake/cli/api/utils/definition_rendering.py +3 -5
  74. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
  75. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
  76. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  77. snowflake/cli/_plugins/nativeapp/manager.py +0 -415
  78. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  79. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  80. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  81. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
  82. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  83. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  84. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,12 +15,13 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import logging
18
+ from io import StringIO
18
19
  from pathlib import Path
19
20
  from textwrap import dedent
20
21
  from typing import List, Optional
21
22
 
22
23
  import typer
23
- from click import MissingParameter
24
+ import yaml
24
25
  from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
25
26
  from snowflake.cli._plugins.nativeapp.common_flags import (
26
27
  ForceOption,
@@ -43,6 +44,24 @@ ws = SnowTyperFactory(
43
44
  log = logging.getLogger(__name__)
44
45
 
45
46
 
47
+ @ws.command(requires_connection=False, hidden=True)
48
+ @with_project_definition()
49
+ def dump(**options):
50
+ """
51
+ Dumps the project definition.
52
+ """
53
+ cli_context = get_cli_context()
54
+ pd = cli_context.project_definition
55
+ io = StringIO()
56
+ yaml.safe_dump(
57
+ pd.model_dump(mode="json", by_alias=True),
58
+ io,
59
+ sort_keys=False,
60
+ width=float("inf"), # Don't break lines
61
+ )
62
+ return MessageResult(io.getvalue())
63
+
64
+
46
65
  @ws.command(requires_connection=True, hidden=True)
47
66
  @with_project_definition()
48
67
  def bundle(
@@ -243,6 +262,11 @@ def version_create(
243
262
  help=f"""The patch number you want to create for an existing version.
244
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.""",
245
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
+ ),
246
270
  skip_git_check: Optional[bool] = typer.Option(
247
271
  False,
248
272
  "--skip-git-check",
@@ -254,8 +278,6 @@ def version_create(
254
278
  **options,
255
279
  ):
256
280
  """Creates a new version for the specified entity."""
257
- if version is None and patch is not None:
258
- raise MissingParameter("Cannot provide a patch without version!")
259
281
 
260
282
  cli_context = get_cli_context()
261
283
  ws = WorkspaceManager(
@@ -267,6 +289,7 @@ def version_create(
267
289
  EntityActions.VERSION_CREATE,
268
290
  version=version,
269
291
  patch=patch,
292
+ label=label,
270
293
  skip_git_check=skip_git_check,
271
294
  interactive=interactive,
272
295
  force=force,
@@ -299,7 +322,7 @@ def version_drop(
299
322
  )
300
323
  ws.perform_action(
301
324
  entity_id,
302
- EntityActions.VERSION_CREATE,
325
+ EntityActions.VERSION_DROP,
303
326
  version=version,
304
327
  interactive=interactive,
305
328
  force=force,
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from pathlib import Path
4
+ from typing import Callable
5
+
6
+ from snowflake.cli.api.console.abc import AbstractConsole
7
+
8
+
9
+ @dataclass
10
+ class WorkspaceContext:
11
+ """
12
+ An object that is passed to each entity when instantiated by WorkspaceManager
13
+ to allow access to the CLI context without requiring the entities to use
14
+ get_cli_context().
15
+ """
16
+
17
+ console: AbstractConsole
18
+ project_root: Path
19
+ get_default_role: Callable[[], str]
20
+ get_default_warehouse: Callable[[], str | None]
21
+
22
+ @cached_property
23
+ def default_role(self) -> str:
24
+ return self.get_default_role()
25
+
26
+ @cached_property
27
+ def default_warehouse(self) -> str | None:
28
+ return self.get_default_warehouse()
29
+
30
+
31
+ @dataclass
32
+ class ActionContext:
33
+ """
34
+ An object that is passed to each action when called by WorkspaceManager
35
+ to provide access to metadata about the entity and project being acted upon.
36
+ """
37
+
38
+ get_entity: Callable
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Dict
3
3
 
4
- from snowflake.cli._plugins.workspace.action_context import ActionContext
4
+ from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
5
5
  from snowflake.cli.api.cli_global_context import get_cli_context
6
6
  from snowflake.cli.api.console import cli_console as cc
7
7
  from snowflake.cli.api.entities.common import EntityActions, get_sql_executor
@@ -31,13 +31,6 @@ class WorkspaceManager:
31
31
  self._entities_cache: Dict[str, Entity] = {}
32
32
  self._project_definition: DefinitionV20 = project_definition
33
33
  self._project_root = project_root
34
- self._default_role = default_role()
35
- if self._default_role is None:
36
- self._default_role = get_sql_executor().current_role()
37
- self.default_warehouse = None
38
- cli_context = get_cli_context()
39
- if cli_context.connection.warehouse:
40
- self.default_warehouse = to_identifier(cli_context.connection.warehouse)
41
34
 
42
35
  def get_entity(self, entity_id: str):
43
36
  """
@@ -50,7 +43,13 @@ class WorkspaceManager:
50
43
  raise ValueError(f"No such entity ID: {entity_id}")
51
44
  entity_model_cls = entity_model.__class__
52
45
  entity_cls = v2_entity_model_to_entity_map[entity_model_cls]
53
- self._entities_cache[entity_id] = entity_cls(entity_model)
46
+ workspace_ctx = WorkspaceContext(
47
+ console=cc,
48
+ project_root=self.project_root,
49
+ get_default_role=_get_default_role,
50
+ get_default_warehouse=_get_default_warehouse,
51
+ )
52
+ self._entities_cache[entity_id] = entity_cls(entity_model, workspace_ctx)
54
53
  return self._entities_cache[entity_id]
55
54
 
56
55
  def perform_action(self, entity_id: str, action: EntityActions, *args, **kwargs):
@@ -60,15 +59,26 @@ class WorkspaceManager:
60
59
  entity = self.get_entity(entity_id)
61
60
  if entity.supports(action):
62
61
  action_ctx = ActionContext(
63
- console=cc,
64
- project_root=self.project_root(),
65
- default_role=self._default_role,
66
- default_warehouse=self.default_warehouse,
67
62
  get_entity=self.get_entity,
68
63
  )
69
64
  return entity.perform(action, action_ctx, *args, **kwargs)
70
65
  else:
71
66
  raise ValueError(f'This entity type does not support "{action.value}"')
72
67
 
68
+ @property
73
69
  def project_root(self) -> Path:
74
70
  return self._project_root
71
+
72
+
73
+ def _get_default_role() -> str:
74
+ role = default_role()
75
+ if role is None:
76
+ role = get_sql_executor().current_role()
77
+ return role
78
+
79
+
80
+ def _get_default_warehouse() -> str | None:
81
+ warehouse = get_cli_context().connection.warehouse
82
+ if warehouse:
83
+ warehouse = to_identifier(warehouse)
84
+ return warehouse
@@ -54,9 +54,9 @@ class _CliGlobalContextManager:
54
54
  project_env_overrides_args: dict[str, str] = field(default_factory=dict)
55
55
 
56
56
  # FIXME: this property only exists to help implement
57
- # nativeapp_definition_v2_to_v1. Consider changing the way
58
- # this calculation is provided to commands in order to remove
59
- # this logic (then make project_definition a non-cloned @property)
57
+ # nativeapp_definition_v2_to_v1 and single_app_and_package.
58
+ # Consider changing the way this calculation is provided to commands
59
+ # in order to remove this logic (then make project_definition a non-cloned @property)
60
60
  override_project_definition: ProjectDefinition | None = None
61
61
 
62
62
  _definition_manager: DefinitionManager | None = None
@@ -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):
@@ -33,6 +33,7 @@ from snowflake.cli.api.connections import ConnectionContext
33
33
  from snowflake.cli.api.console import cli_console
34
34
  from snowflake.cli.api.identifiers import FQN
35
35
  from snowflake.cli.api.output.formats import OutputFormat
36
+ from snowflake.cli.api.stage_path import StagePath
36
37
 
37
38
  DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
38
39
 
@@ -83,7 +84,7 @@ ConnectionOption = typer.Option(
83
84
  "--connection",
84
85
  "-c",
85
86
  "--environment",
86
- help=f"Name of the connection, as defined in your `config.toml`. Default: `default`.",
87
+ help=f"Name of the connection, as defined in your `config.toml` file. Default: `default`.",
87
88
  callback=_connection_callback("connection_name"),
88
89
  show_default=False,
89
90
  rich_help_panel=_CONNECTION_SECTION,
@@ -276,7 +277,7 @@ MfaPasscodeOption = typer.Option(
276
277
  EnableDiagOption = typer.Option(
277
278
  False,
278
279
  "--enable-diag",
279
- help="Run python connector diagnostic test",
280
+ help="Run Python connector diagnostic test",
280
281
  callback=_connection_callback("enable_diag"),
281
282
  show_default=False,
282
283
  is_flag=True,
@@ -349,7 +350,7 @@ VerboseOption = typer.Option(
349
350
  DebugOption = typer.Option(
350
351
  False,
351
352
  "--debug",
352
- help="Displays log entries for log levels `debug` and higher; debug logs contains additional information.",
353
+ help="Displays log entries for log levels `debug` and higher; debug logs contain additional information.",
353
354
  callback=_context_callback("enable_tracebacks"),
354
355
  is_flag=True,
355
356
  rich_help_panel=_CLI_BEHAVIOUR,
@@ -406,9 +407,9 @@ def variables_option(description: str):
406
407
 
407
408
 
408
409
  ExecuteVariablesOption = variables_option(
409
- 'Variables for the execution context. For example: `-D "<key>=<value>"`. '
410
- "For SQL files variables are use to expand the template and any unknown variable will cause an error. "
411
- "For Python files variables are used to update os.environ dictionary. Provided keys are capitalized to adhere to best practices."
410
+ 'Variables for the execution context; for example: `-D "<key>=<value>"`. '
411
+ "For SQL files, variables are used to expand the template, and any unknown variable will cause an error (consider embedding quoting in the file)."
412
+ "For Python files, variables are used to update the os.environ dictionary. Provided keys are capitalized to adhere to best practices. "
412
413
  "In case of SQL files string values must be quoted in `''` (consider embedding quoting in the file).",
413
414
  )
414
415
 
@@ -473,6 +474,13 @@ class IdentifierStageType(click.ParamType):
473
474
  return FQN.from_stage(value)
474
475
 
475
476
 
477
+ class IdentifierStagePathType(click.ParamType):
478
+ name = "TEXT"
479
+
480
+ def convert(self, value, param, ctx):
481
+ return StagePath.from_stage_str(value)
482
+
483
+
476
484
  def identifier_argument(
477
485
  sf_object: str,
478
486
  example: str,
@@ -481,7 +489,7 @@ def identifier_argument(
481
489
  ) -> typer.Argument:
482
490
  return typer.Argument(
483
491
  ...,
484
- help=f"Identifier of the {sf_object}. For example: {example}",
492
+ help=f"Identifier of the {sf_object}; for example: {example}",
485
493
  show_default=False,
486
494
  click_type=click_type,
487
495
  callback=callback,
@@ -496,6 +504,14 @@ def identifier_stage_argument(
496
504
  )
497
505
 
498
506
 
507
+ def identifier_stage_path_argument(
508
+ sf_object: str, example: str, callback: Callable | None = None
509
+ ) -> typer.Argument:
510
+ return identifier_argument(
511
+ sf_object, example, click_type=IdentifierStagePathType(), callback=callback
512
+ )
513
+
514
+
499
515
  def execution_identifier_argument(sf_object: str, example: str) -> typer.Argument:
500
516
  return typer.Argument(
501
517
  ...,
@@ -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,16 +340,34 @@ 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:
349
- if CONNECTIONS_FILE.exists() and not file_permissions_are_strict(CONNECTIONS_FILE):
350
- raise ConfigFileTooWidePermissionsError(CONNECTIONS_FILE)
351
- if CONFIG_FILE.exists() and not file_permissions_are_strict(CONFIG_FILE):
352
- raise ConfigFileTooWidePermissionsError(CONFIG_FILE)
364
+ if not IS_WINDOWS:
365
+ if CONNECTIONS_FILE.exists() and not file_permissions_are_strict(
366
+ CONNECTIONS_FILE
367
+ ):
368
+ raise ConfigFileTooWidePermissionsError(CONNECTIONS_FILE)
369
+ if CONFIG_FILE.exists() and not file_permissions_are_strict(CONFIG_FILE):
370
+ raise ConfigFileTooWidePermissionsError(CONFIG_FILE)
353
371
 
354
372
 
355
373
  from typing import Literal
@@ -370,9 +388,12 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
370
388
  return {k: _bool_or_unknown(v) for k, v in flags.items()}
371
389
 
372
390
 
391
+ def _read_config_file_toml() -> dict:
392
+ return tomlkit.loads(CONFIG_MANAGER.file_path.read_text()).unwrap()
393
+
394
+
373
395
  def _read_connections_toml() -> dict:
374
- with open(CONNECTIONS_FILE, "r") as f:
375
- return tomlkit.loads(f.read()).unwrap()
396
+ return tomlkit.loads(CONNECTIONS_FILE.read_text()).unwrap()
376
397
 
377
398
 
378
399
  def _update_connections_toml(connections: dict):
@@ -22,7 +22,7 @@ from dataclasses import asdict, dataclass, field, fields, replace
22
22
  from pathlib import Path
23
23
  from typing import Optional
24
24
 
25
- from snowflake.cli.api.config import get_default_connection_name
25
+ from snowflake.cli.api.config import get_connection_dict, get_default_connection_name
26
26
  from snowflake.cli.api.exceptions import InvalidSchemaError
27
27
  from snowflake.connector import SnowflakeConnection
28
28
  from snowflake.connector.compat import IS_WINDOWS
@@ -79,6 +79,17 @@ class ConnectionContext:
79
79
  raise KeyError(f"{key} is not a field of {self.__class__.__name__}")
80
80
  setattr(self, key, value)
81
81
 
82
+ def update_from_config(self) -> ConnectionContext:
83
+ connection_config = get_connection_dict(connection_name=self.connection_name)
84
+ if "private_key_path" in connection_config:
85
+ connection_config["private_key_file"] = connection_config[
86
+ "private_key_path"
87
+ ]
88
+ del connection_config["private_key_path"]
89
+
90
+ self.update(**connection_config)
91
+ return self
92
+
82
93
  def __repr__(self) -> str:
83
94
  """Minimal repr where None values have their keys omitted."""
84
95
  items = [f"{k}={repr(v)}" for (k, v) in self.present_values_as_dict().items()]
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from typing import Generic, Type, TypeVar, get_args
3
3
 
4
- from snowflake.cli._plugins.workspace.action_context import ActionContext
4
+ from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
5
5
  from snowflake.cli.api.sql_execution import SqlExecutor
6
6
 
7
7
 
@@ -10,6 +10,7 @@ class EntityActions(str, Enum):
10
10
  DEPLOY = "action_deploy"
11
11
  DROP = "action_drop"
12
12
  VALIDATE = "action_validate"
13
+ EVENTS = "action_events"
13
14
 
14
15
  VERSION_LIST = "action_version_list"
15
16
  VERSION_CREATE = "action_version_create"
@@ -24,8 +25,9 @@ class EntityBase(Generic[T]):
24
25
  Base class for the fully-featured entity classes.
25
26
  """
26
27
 
27
- def __init__(self, entity_model: T):
28
+ def __init__(self, entity_model: T, workspace_ctx: WorkspaceContext):
28
29
  self._entity_model = entity_model
30
+ self._workspace_ctx = workspace_ctx
29
31
 
30
32
  @classmethod
31
33
  def get_entity_model_type(cls) -> Type[T]:
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  from pathlib import Path
3
- from textwrap import dedent
4
3
  from typing import Any, List, NoReturn, Optional
5
4
 
6
5
  import jinja2
@@ -13,10 +12,11 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
13
12
  InvalidTemplateInFileError,
14
13
  MissingScriptError,
15
14
  )
15
+ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
16
16
  from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
17
17
  from snowflake.cli._plugins.stage.diff import (
18
18
  DiffResult,
19
- StagePath,
19
+ StagePathType,
20
20
  compute_stage_diff,
21
21
  preserve_from_diff,
22
22
  sync_local_diff_with_stage,
@@ -30,7 +30,11 @@ from snowflake.cli.api.errno import (
30
30
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
31
31
  NO_WAREHOUSE_SELECTED_IN_SESSION,
32
32
  )
33
- from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
33
+ from snowflake.cli.api.exceptions import (
34
+ DoesNotExistOrUnauthorizedError,
35
+ NoWarehouseSelectedInSessionError,
36
+ SnowflakeSQLExecutionError,
37
+ )
34
38
  from snowflake.cli.api.metrics import CLICounterField
35
39
  from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
36
40
  from snowflake.cli.api.rendering.sql_templates import (
@@ -41,46 +45,21 @@ from snowflake.connector import ProgrammingError
41
45
  from snowflake.connector.cursor import SnowflakeCursor
42
46
 
43
47
 
44
- def generic_sql_error_handler(
45
- err: ProgrammingError, role: Optional[str] = None, warehouse: Optional[str] = None
46
- ) -> NoReturn:
48
+ def generic_sql_error_handler(err: ProgrammingError) -> NoReturn:
47
49
  # Potential refactor: If moving away from Python 3.8 and 3.9 to >= 3.10, use match ... case
48
- if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
49
- raise ProgrammingError(
50
- msg=dedent(
51
- f"""\
52
- Received error message '{err.msg}' while executing SQL statement.
53
- '{role}' may not have access to warehouse '{warehouse}'.
54
- Please grant usage privilege on warehouse to this role.
55
- """
56
- ),
57
- errno=err.errno,
58
- )
50
+ if (
51
+ err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED
52
+ or "does not exist or not authorized" in err.msg
53
+ ):
54
+ raise DoesNotExistOrUnauthorizedError(msg=err.msg) from err
59
55
  elif err.errno == NO_WAREHOUSE_SELECTED_IN_SESSION:
60
- raise ProgrammingError(
61
- msg=dedent(
62
- f"""\
63
- Received error message '{err.msg}' while executing SQL statement.
64
- Please provide a warehouse for the active session role in your project definition file, config.toml file, or via command line.
65
- """
66
- ),
67
- errno=err.errno,
68
- )
69
- elif "does not exist or not authorized" in err.msg:
70
- raise ProgrammingError(
71
- msg=dedent(
72
- f"""\
73
- Received error message '{err.msg}' while executing SQL statement.
74
- Please check the name of the resource you are trying to query or the permissions of the role you are using to run the query.
75
- """
76
- )
77
- )
56
+ raise NoWarehouseSelectedInSessionError(msg=err.msg) from err
78
57
  raise err
79
58
 
80
59
 
81
60
  def _get_stage_paths_to_sync(
82
61
  local_paths_to_sync: List[Path], deploy_root: Path
83
- ) -> List[StagePath]:
62
+ ) -> List[StagePathType]:
84
63
  """
85
64
  Takes a list of paths (files and directories), returning a list of all files recursively relative to the deploy root.
86
65
  """
@@ -128,22 +107,15 @@ def sync_deploy_root_with_stage(
128
107
  A `DiffResult` instance describing the changes that were performed.
129
108
  """
130
109
 
131
- sql_executor = get_sql_executor()
110
+ sql_facade = get_snowflake_facade()
132
111
  # Does a stage already exist within the application package, or we need to create one?
133
112
  # Using "if not exists" should take care of either case.
134
113
  console.step(
135
114
  f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
136
115
  )
137
- with sql_executor.use_role(role):
138
- sql_executor.execute_query(
139
- f"create schema if not exists {package_name}.{stage_schema}"
140
- )
141
- sql_executor.execute_query(
142
- f"""
143
- create stage if not exists {stage_fqn}
144
- encryption = (TYPE = 'SNOWFLAKE_SSE')
145
- DIRECTORY = (ENABLE = TRUE)"""
146
- )
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)
147
119
 
148
120
  # Perform a diff operation and display results to the user for informational purposes
149
121
  if print_diff:
@@ -217,36 +189,24 @@ def sync_deploy_root_with_stage(
217
189
  return diff
218
190
 
219
191
 
220
- def _execute_sql_script(
221
- script_content: str,
222
- database_name: Optional[str] = None,
223
- ) -> None:
224
- """
225
- Executing the provided SQL script content.
226
- This assumes that a relevant warehouse is already active.
227
- If database_name is passed in, it will be used first.
228
- """
229
- try:
230
- sql_executor = get_sql_executor()
231
- if database_name is not None:
232
- sql_executor.execute_query(f"use database {database_name}")
233
- sql_executor.execute_queries(script_content)
234
- except ProgrammingError as err:
235
- generic_sql_error_handler(err)
236
-
237
-
238
192
  def execute_post_deploy_hooks(
239
193
  console: AbstractConsole,
240
194
  project_root: Path,
241
195
  post_deploy_hooks: Optional[List[PostDeployHook]],
242
196
  deployed_object_type: str,
197
+ role_name: str,
243
198
  database_name: str,
199
+ warehouse_name: str,
244
200
  ) -> None:
245
201
  """
246
202
  Executes post-deploy hooks for the given object type.
247
203
  While executing SQL post deploy hooks, it first switches to the database provided in the input.
248
204
  All post deploy scripts templates will first be expanded using the global template context.
249
205
  """
206
+ get_cli_context().metrics.set_counter_default(
207
+ CLICounterField.POST_DEPLOY_SCRIPTS, 0
208
+ )
209
+
250
210
  if not post_deploy_hooks:
251
211
  return
252
212
 
@@ -254,9 +214,11 @@ def execute_post_deploy_hooks(
254
214
 
255
215
  with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
256
216
  sql_scripts_paths = []
217
+ display_paths = []
257
218
  for hook in post_deploy_hooks:
258
219
  if hook.sql_script:
259
220
  sql_scripts_paths.append(hook.sql_script)
221
+ display_paths.append(hook.display_path)
260
222
  else:
261
223
  raise ValueError(
262
224
  f"Unsupported {deployed_object_type} post-deploy hook type: {hook}"
@@ -268,11 +230,16 @@ def execute_post_deploy_hooks(
268
230
  sql_scripts_paths,
269
231
  )
270
232
 
271
- for index, sql_script_path in enumerate(sql_scripts_paths):
233
+ sql_facade = get_snowflake_facade()
234
+
235
+ for index, sql_script_path in enumerate(display_paths):
272
236
  console.step(f"Executing SQL script: {sql_script_path}")
273
- _execute_sql_script(
274
- script_content=scripts_content_list[index],
275
- 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,
276
243
  )
277
244
 
278
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