snowflake-cli 3.2.2__py3-none-any.whl → 3.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_app/constants.py +4 -0
  6. snowflake/cli/_app/snow_connector.py +12 -0
  7. snowflake/cli/_app/telemetry.py +10 -3
  8. snowflake/cli/_plugins/connection/util.py +12 -19
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +207 -1
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
  19. snowflake/cli/_plugins/nativeapp/commands.py +113 -21
  20. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
  23. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  24. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  25. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  26. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
  27. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  28. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
  29. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  30. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  31. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
  32. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
  35. snowflake/cli/_plugins/notebook/commands.py +55 -2
  36. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  37. snowflake/cli/_plugins/notebook/manager.py +7 -5
  38. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  39. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  40. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  41. snowflake/cli/_plugins/notebook/types.py +3 -0
  42. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  43. snowflake/cli/_plugins/snowpark/common.py +47 -2
  44. snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
  45. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  46. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  47. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  48. snowflake/cli/_plugins/spcs/common.py +129 -0
  49. snowflake/cli/_plugins/spcs/services/commands.py +131 -14
  50. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  51. snowflake/cli/_plugins/stage/commands.py +2 -1
  52. snowflake/cli/_plugins/stage/diff.py +60 -39
  53. snowflake/cli/_plugins/stage/manager.py +34 -13
  54. snowflake/cli/_plugins/stage/utils.py +1 -1
  55. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  56. snowflake/cli/_plugins/streamlit/manager.py +70 -22
  57. snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
  58. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  59. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  60. snowflake/cli/_plugins/workspace/commands.py +6 -5
  61. snowflake/cli/_plugins/workspace/manager.py +9 -5
  62. snowflake/cli/api/artifacts/__init__.py +13 -0
  63. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  64. snowflake/cli/api/artifacts/common.py +78 -0
  65. snowflake/cli/api/artifacts/utils.py +82 -0
  66. snowflake/cli/api/cli_global_context.py +36 -2
  67. snowflake/cli/api/commands/flags.py +10 -4
  68. snowflake/cli/api/commands/utils.py +28 -2
  69. snowflake/cli/api/config.py +6 -2
  70. snowflake/cli/api/connections.py +12 -1
  71. snowflake/cli/api/constants.py +10 -1
  72. snowflake/cli/api/entities/common.py +81 -14
  73. snowflake/cli/api/entities/resolver.py +160 -0
  74. snowflake/cli/api/entities/utils.py +65 -23
  75. snowflake/cli/api/errno.py +63 -3
  76. snowflake/cli/api/feature_flags.py +19 -4
  77. snowflake/cli/api/metrics.py +21 -27
  78. snowflake/cli/api/project/definition_conversion.py +4 -4
  79. snowflake/cli/api/project/project_paths.py +28 -0
  80. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  81. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  82. snowflake/cli/api/project/schemas/project_definition.py +54 -6
  83. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  84. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  85. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  86. snowflake/cli/api/project/util.py +45 -0
  87. snowflake/cli/api/secure_path.py +6 -0
  88. snowflake/cli/api/sql_execution.py +5 -1
  89. snowflake/cli/api/stage_path.py +7 -2
  90. snowflake/cli/api/utils/graph.py +3 -0
  91. snowflake/cli/api/utils/path_utils.py +24 -0
  92. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
  93. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
  94. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
  95. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  96. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -17,8 +17,9 @@ from __future__ import annotations
17
17
  from contextlib import contextmanager
18
18
  from contextvars import ContextVar
19
19
  from dataclasses import dataclass, field, replace
20
+ from functools import wraps
20
21
  from pathlib import Path
21
- from typing import TYPE_CHECKING, Iterator
22
+ from typing import TYPE_CHECKING, Iterator, Optional
22
23
 
23
24
  from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
24
25
  from snowflake.cli.api.exceptions import MissingConfiguration
@@ -157,7 +158,7 @@ class _CliGlobalContextAccess:
157
158
  return self._manager.enable_tracebacks
158
159
 
159
160
  @property
160
- def metrics(self):
161
+ def metrics(self) -> CLIMetrics:
161
162
  return self._manager.metrics
162
163
 
163
164
  @property
@@ -195,6 +196,19 @@ class _CliGlobalContextAccess:
195
196
  """Computes whether cli_console output should be muted."""
196
197
  return self._manager.output_format == OutputFormat.JSON
197
198
 
199
+ @property
200
+ def snow_api_root(
201
+ self,
202
+ ) -> Optional[
203
+ object
204
+ ]: # Should be Optional[Root], but we need local import for performance reasons
205
+ from snowflake.core import Root
206
+
207
+ if self.connection:
208
+ return Root(self.connection)
209
+ else:
210
+ return None
211
+
198
212
 
199
213
  _CLI_CONTEXT_MANAGER: ContextVar[_CliGlobalContextManager | None] = ContextVar(
200
214
  "cli_context", default=None
@@ -213,6 +227,26 @@ def get_cli_context() -> _CliGlobalContextAccess:
213
227
  return _CliGlobalContextAccess(get_cli_context_manager())
214
228
 
215
229
 
230
+ def span(span_name: str):
231
+ """
232
+ Decorator to start a command metrics span that encompasses a whole function
233
+
234
+ Must be used instead of directly calling @get_cli_context().metrics.span(span_name)
235
+ as a decorator to ensure that the cli context is grabbed at run time instead of at
236
+ module load time, which would not reflect forking
237
+ """
238
+
239
+ def decorator(func):
240
+ @wraps(func)
241
+ def wrapper(*args, **kwargs):
242
+ with get_cli_context().metrics.span(span_name):
243
+ return func(*args, **kwargs)
244
+
245
+ return wrapper
246
+
247
+ return decorator
248
+
249
+
216
250
  @contextmanager
217
251
  def fork_cli_context(
218
252
  connection_overrides: dict | None = None,
@@ -95,7 +95,7 @@ TemporaryConnectionOption = typer.Option(
95
95
  False,
96
96
  "--temporary-connection",
97
97
  "-x",
98
- help="Uses connection defined with command line parameters, instead of one defined in config",
98
+ help="Uses a connection defined with command line parameters, instead of one defined in config",
99
99
  callback=_connection_callback("temporary_connection"),
100
100
  is_flag=True,
101
101
  rich_help_panel=_CONNECTION_SECTION,
@@ -383,6 +383,12 @@ ReplaceOption = OverrideableOption(
383
383
  mutually_exclusive=CREATE_MODE_OPTION_NAMES,
384
384
  )
385
385
 
386
+ ForceReplaceOption = OverrideableOption(
387
+ False,
388
+ "--force-replace",
389
+ help="Replace this object, even if the state didn't change",
390
+ )
391
+
386
392
  OnErrorOption = typer.Option(
387
393
  OnErrorType.BREAK.value,
388
394
  "--on-error",
@@ -531,8 +537,8 @@ def project_definition_option(is_optional: bool):
531
537
  None,
532
538
  "-p",
533
539
  "--project",
534
- help=f"Path where Snowflake project resides. "
535
- f"Defaults to current working directory.",
540
+ help=f"Path where the Snowflake project is stored. "
541
+ f"Defaults to the current working directory.",
536
542
  callback=project_path_callback,
537
543
  show_default=False,
538
544
  )
@@ -551,7 +557,7 @@ def project_env_overrides_option():
551
557
  return typer.Option(
552
558
  [],
553
559
  "--env",
554
- help="String in format of key=value. Overrides variables from env section used for templates.",
560
+ help="String in the format key=value. Overrides variables from the env section used for templates.",
555
561
  callback=project_env_overrides_callback,
556
562
  show_default=False,
557
563
  )
@@ -1,7 +1,9 @@
1
- from typing import List, Optional
1
+ from typing import Dict, List, Optional
2
2
 
3
- from click import ClickException
3
+ from click import ClickException, UsageError
4
4
  from snowflake.cli.api.commands.common import Variable
5
+ from snowflake.cli.api.exceptions import NoProjectDefinitionError
6
+ from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
5
7
 
6
8
 
7
9
  def parse_key_value_variables(variables: Optional[List[str]]) -> List[Variable]:
@@ -16,3 +18,27 @@ def parse_key_value_variables(variables: Optional[List[str]]) -> List[Variable]:
16
18
  key, value = p.split("=", 1)
17
19
  result.append(Variable(key.strip(), value.strip()))
18
20
  return result
21
+
22
+
23
+ def get_entity_for_operation(
24
+ cli_context,
25
+ entity_id: str | None,
26
+ project_definition,
27
+ entity_type: str,
28
+ ):
29
+ entities: Dict[str, EntityModelBase] = project_definition.get_entities_by_type(
30
+ entity_type=entity_type
31
+ )
32
+ if not entities:
33
+ raise NoProjectDefinitionError(
34
+ project_type=entity_type, project_root=cli_context.project_root
35
+ )
36
+ if entity_id and entity_id not in entities:
37
+ raise UsageError(f"No '{entity_id}' entity in project definition file.")
38
+ if len(entities.keys()) == 1:
39
+ entity_id = list(entities.keys())[0]
40
+ if entity_id is None:
41
+ raise UsageError(
42
+ f"Multiple entities of type {entity_type} found. Please provide entity id for the operation."
43
+ )
44
+ return entities[entity_id]
@@ -286,8 +286,12 @@ def get_config_value(*path, key: str, default: Optional[Any] = Empty) -> Any:
286
286
  raise
287
287
 
288
288
 
289
- def get_config_bool_value(*path, key: str, default: Optional[Any] = Empty) -> bool:
290
- value = get_config_value(*path, key=key, default=default)
289
+ def get_config_bool_value(*path, key: str, default: Optional[bool]) -> Optional[bool]:
290
+ value = get_config_value(*path, key=key, default=None)
291
+
292
+ if value is None:
293
+ return default
294
+
291
295
  try:
292
296
  return try_cast_to_bool(value)
293
297
  except ValueError:
@@ -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 merge_with_config(self, **updates) -> ConnectionContext:
83
+ """
84
+ Updates missing fields from the config, but does not overwrite existing.
85
+ """
86
+ field_map = {field.name for field in fields(self)}
87
+ for key, value in updates.items():
88
+ if key in field_map and getattr(self, key) is None:
89
+ setattr(self, key, value)
90
+
91
+ return self
92
+
82
93
  def update_from_config(self) -> ConnectionContext:
83
94
  connection_config = get_connection_dict(connection_name=self.connection_name)
84
95
  if "private_key_path" in connection_config:
@@ -87,7 +98,7 @@ class ConnectionContext:
87
98
  ]
88
99
  del connection_config["private_key_path"]
89
100
 
90
- self.update(**connection_config)
101
+ self.merge_with_config(**connection_config)
91
102
  return self
92
103
 
93
104
  def __repr__(self) -> str:
@@ -45,6 +45,7 @@ class ObjectType(Enum):
45
45
  )
46
46
  # JOB = ObjectNames("job", "job", "jobs")
47
47
  NETWORK_RULE = ObjectNames("network-rule", "network rule", "network rules")
48
+ NOTEBOOK = ObjectNames("notebook", "notebook", "notebooks")
48
49
  PROCEDURE = ObjectNames("procedure", "procedure", "procedures")
49
50
  ROLE = ObjectNames("role", "role", "roles")
50
51
  SCHEMA = ObjectNames("schema", "schema", "schemas")
@@ -62,6 +63,10 @@ class ObjectType(Enum):
62
63
  "image-repository", "image repository", "image repositories"
63
64
  )
64
65
  GIT_REPOSITORY = ObjectNames("git-repository", "git repository", "git repositories")
66
+ APPLICATION = ObjectNames("application", "application", "applications")
67
+ APPLICATION_PACKAGE = ObjectNames(
68
+ "application-package", "application package", "application packages"
69
+ )
65
70
 
66
71
  def __str__(self):
67
72
  """This makes using this Enum easier in formatted string"""
@@ -69,7 +74,11 @@ class ObjectType(Enum):
69
74
 
70
75
 
71
76
  OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType}
72
- SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys())
77
+ UNSUPPORTED_OBJECTS = {
78
+ ObjectType.APPLICATION.value.cli_name,
79
+ ObjectType.APPLICATION_PACKAGE.value.cli_name,
80
+ }
81
+ SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS)
73
82
 
74
83
  # Scope names here must replace spaces with '-'. For example 'compute pool' is 'compute-pool'.
75
84
  VALID_SCOPES = ["database", "schema", "compute-pool"]
@@ -1,23 +1,42 @@
1
- from enum import Enum
1
+ import functools
2
+ from pathlib import Path
2
3
  from typing import Generic, Type, TypeVar, get_args
3
4
 
4
5
  from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
6
+ from snowflake.cli.api.cli_global_context import get_cli_context, span
7
+ from snowflake.cli.api.entities.resolver import DependencyResolver
8
+ from snowflake.cli.api.entities.utils import EntityActions, get_sql_executor
9
+ from snowflake.cli.api.identifiers import FQN
5
10
  from snowflake.cli.api.sql_execution import SqlExecutor
11
+ from snowflake.connector import SnowflakeConnection
12
+ from snowflake.connector.cursor import SnowflakeCursor
6
13
 
14
+ T = TypeVar("T")
7
15
 
8
- class EntityActions(str, Enum):
9
- BUNDLE = "action_bundle"
10
- DEPLOY = "action_deploy"
11
- DROP = "action_drop"
12
- VALIDATE = "action_validate"
13
- EVENTS = "action_events"
14
16
 
15
- VERSION_LIST = "action_version_list"
16
- VERSION_CREATE = "action_version_create"
17
- VERSION_DROP = "action_version_drop"
17
+ def attach_spans_to_entity_actions(entity_name: str):
18
+ """
19
+ Class decorator for EntityBase subclasses to automatically wrap
20
+ every implemented entity action method with a metrics span
18
21
 
22
+ Args:
23
+ entity_name (str): Custom name for entity type to be displayed in metrics
24
+ """
19
25
 
20
- T = TypeVar("T")
26
+ def decorator(cls: type[T]) -> type[T]:
27
+ for attr_name, attr_value in vars(cls).items():
28
+ is_entity_action = attr_name in [
29
+ enum_member for enum_member in EntityActions
30
+ ]
31
+
32
+ if is_entity_action and callable(attr_value):
33
+ attr_name_without_action_prefix = attr_name.partition("_")[2]
34
+ span_name = f"action.{entity_name}.{attr_name_without_action_prefix}"
35
+ action_with_span = span(span_name)(attr_value)
36
+ setattr(cls, attr_name, action_with_span)
37
+ return cls
38
+
39
+ return decorator
21
40
 
22
41
 
23
42
  class EntityBase(Generic[T]):
@@ -28,6 +47,11 @@ class EntityBase(Generic[T]):
28
47
  def __init__(self, entity_model: T, workspace_ctx: WorkspaceContext):
29
48
  self._entity_model = entity_model
30
49
  self._workspace_ctx = workspace_ctx
50
+ self.dependency_resolver = DependencyResolver(entity_model)
51
+
52
+ @property
53
+ def entity_id(self) -> str:
54
+ return self._entity_model.entity_id # type: ignore
31
55
 
32
56
  @classmethod
33
57
  def get_entity_model_type(cls) -> Type[T]:
@@ -49,10 +73,53 @@ class EntityBase(Generic[T]):
49
73
  ):
50
74
  """
51
75
  Performs the requested action.
76
+ This is a preferred way to perform actions on entities, over calling actions directly,
77
+ as it will also call the dependencies in the correct order.
52
78
  """
79
+ self.dependency_resolver.perform_for_dep(action, action_ctx, *args, **kwargs)
53
80
  return getattr(self, action)(action_ctx, *args, **kwargs)
54
81
 
82
+ @property
83
+ def root(self) -> Path:
84
+ return self._workspace_ctx.project_root
85
+
86
+ @property
87
+ def identifier(self) -> str:
88
+ return self.model.fqn.sql_identifier
89
+
90
+ @property
91
+ def fqn(self) -> FQN:
92
+ return self._entity_model.fqn # type: ignore[attr-defined]
93
+
94
+ @functools.cached_property
95
+ def _sql_executor(
96
+ self,
97
+ ) -> SqlExecutor:
98
+ return get_sql_executor()
99
+
100
+ def _execute_query(self, sql: str) -> SnowflakeCursor:
101
+ return self._sql_executor.execute_query(sql)
102
+
103
+ @functools.cached_property
104
+ def _conn(self) -> SnowflakeConnection:
105
+ return self._sql_executor._conn # noqa
106
+
107
+ @property
108
+ def snow_api_root(self):
109
+ return get_cli_context().snow_api_root
110
+
111
+ @property
112
+ def model(self):
113
+ return self._entity_model
114
+
115
+ def dependent_entities(self, action_ctx: ActionContext):
116
+ return self.dependency_resolver.depends_on(action_ctx)
117
+
118
+ def get_usage_grant_sql(self, app_role: str) -> str:
119
+ return f"GRANT USAGE ON {self.model.type.upper()} {self.identifier} TO ROLE {app_role};"
120
+
121
+ def get_describe_sql(self) -> str:
122
+ return f"DESCRIBE {self.model.type.upper()} {self.identifier};"
55
123
 
56
- def get_sql_executor() -> SqlExecutor:
57
- """Returns an SQL Executor that uses the connection from the current CLI context"""
58
- return SqlExecutor()
124
+ def get_drop_sql(self) -> str:
125
+ return f"DROP {self.model.type.upper()} {self.identifier};"
@@ -0,0 +1,160 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Dict, List, Tuple
3
+
4
+ from snowflake.cli._plugins.workspace.context import ActionContext
5
+ from snowflake.cli.api.entities.utils import EntityActions
6
+ from snowflake.cli.api.exceptions import CycleDetectedError
7
+ from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
8
+ from snowflake.cli.api.utils.graph import Graph, Node
9
+
10
+
11
+ @dataclass
12
+ class Dependency:
13
+ entity_id: str
14
+ call_arguments: Dict[str, Any]
15
+
16
+ def __eq__(self, other):
17
+ return self.entity_id == other.entity_id
18
+
19
+ def __hash__(self):
20
+ return hash(self.entity_id)
21
+
22
+
23
+ class DependencyResolver:
24
+ """
25
+ Base class for resolving dependencies logic.
26
+ Any logic for resolving dependencies, calling their actions or validating them should be implemented here.
27
+ If an entity uses it's specific logic, it should implement its own resolver, inheriting from this one
28
+ """
29
+
30
+ def __init__(self, model: EntityModelBase):
31
+ self.entity_model = model
32
+ self.dependencies: List[Dependency] = []
33
+
34
+ def depends_on(self, action_ctx: ActionContext) -> List[Dependency]:
35
+ """
36
+ Returns a list of entities that this entity depends on.
37
+ The list is sorted in order they should be called- last one depends on all the previous.
38
+ """
39
+ if not self.dependencies:
40
+ graph = self._create_dependency_graph(action_ctx)
41
+ self.dependencies = self._check_and_sort_dependencies(graph)
42
+
43
+ return self.dependencies
44
+
45
+ def perform_for_dep(
46
+ self, action: EntityActions, action_ctx: ActionContext, *args, **kwargs
47
+ ):
48
+ """
49
+ Method used to perform selected
50
+ """
51
+ for dependency in self.depends_on(action_ctx):
52
+ entity = action_ctx.get_entity(dependency.entity_id)
53
+ if entity.supports(action):
54
+ arguments = dependency.call_arguments.get(action.get_action_name, {})
55
+ getattr(entity, action)(action_ctx, **arguments)
56
+
57
+ def _create_dependency_graph(self, action_ctx: ActionContext) -> Graph[Dependency]:
58
+ """
59
+ Creates a graph for dependencies. We need the graph, instead of a simple list, because we need to check if
60
+ calling dependencies actions in selected order is possible.
61
+ """
62
+ graph = Graph()
63
+ depends_on = self.entity_model.meta.depends_on if self.entity_model.meta else [] # type: ignore
64
+ self_dependency = Dependency(entity_id=self.entity_model.entity_id, call_arguments={}) # type: ignore
65
+ resolved_nodes = set()
66
+
67
+ graph.add(Node(key=self_dependency.entity_id, data=self_dependency))
68
+
69
+ def _resolve_dependencies(parent_id: str, dependency_id: str) -> None:
70
+
71
+ (
72
+ child_dependencies,
73
+ call_arguments,
74
+ ) = self._get_child_dependencies_and_call_arguments(
75
+ dependency_id=dependency_id, action_ctx=action_ctx
76
+ )
77
+
78
+ if not graph.contains_node(dependency_id):
79
+ dependency_node = Node(
80
+ key=dependency_id,
81
+ data=Dependency(
82
+ entity_id=dependency_id, call_arguments=call_arguments
83
+ ),
84
+ )
85
+ graph.add(dependency_node)
86
+
87
+ graph.add_directed_edge(parent_id, dependency_id)
88
+
89
+ resolved_nodes.add(dependency_node.key)
90
+
91
+ for child_dependency in child_dependencies:
92
+ if child_dependency not in resolved_nodes:
93
+ _resolve_dependencies(dependency_node.key, child_dependency)
94
+ else:
95
+ graph.add_directed_edge(dependency_node.key, child_dependency)
96
+
97
+ for dependency in depends_on:
98
+ _resolve_dependencies(self_dependency.entity_id, dependency)
99
+
100
+ return graph
101
+
102
+ @staticmethod
103
+ def _check_and_sort_dependencies(
104
+ graph: Graph[Dependency],
105
+ ) -> List[Dependency]:
106
+ """
107
+ This function is used to check and organize the dependency list.
108
+ The check has two stages:
109
+ * Cycle detection in dependency
110
+ * Clearing duplicate
111
+
112
+ In the first stage, if cycle is detected, it raises CycleDetectedError with node causing it specified.
113
+ The result list, shows entities this one depends on, in order they should be called.
114
+ Duplicates are removed in a way, that preserves earliest possible call.
115
+ Last item is removed from the result list, as it is this entity itself.
116
+ """
117
+ result = []
118
+
119
+ def _on_cycle(node: Node[Dependency]) -> None:
120
+ raise CycleDetectedError(
121
+ f"Cycle detected in entity dependencies: {node.key}"
122
+ )
123
+
124
+ def _on_visit(node: Node[Dependency]) -> None:
125
+ result.append(node.data)
126
+
127
+ graph.dfs(on_cycle_action=_on_cycle, visit_action=_on_visit)
128
+
129
+ return clear_duplicates_from_list(result)[:-1]
130
+
131
+ @staticmethod
132
+ def _get_child_dependencies_and_call_arguments(
133
+ dependency_id: str, action_ctx: ActionContext
134
+ ) -> Tuple[List[str], Dict[str, Any]]:
135
+ child_dependency = action_ctx.get_entity(dependency_id)
136
+
137
+ if not child_dependency:
138
+ raise ValueError(f"Entity with id {dependency_id} not found in project")
139
+
140
+ if child_dependency.model.meta:
141
+ return (
142
+ child_dependency.model.meta.depends_on,
143
+ child_dependency.model.meta.action_arguments,
144
+ )
145
+
146
+ else:
147
+ return [], {}
148
+
149
+ @staticmethod
150
+ def get_action_name(action: EntityActions) -> str:
151
+
152
+ return action.value.split("_")[1]
153
+
154
+
155
+ def clear_duplicates_from_list(input_list: list[Any]) -> list[Any]:
156
+ """
157
+ Removes duplicates from the input list, preserving the first occurrence.
158
+ """
159
+ seen = set()
160
+ return [x for x in input_list if not (x in seen or seen.add(x))] # type: ignore
@@ -1,13 +1,10 @@
1
1
  import os
2
+ from enum import Enum
2
3
  from pathlib import Path
3
4
  from typing import Any, List, NoReturn, Optional
4
5
 
5
6
  import jinja2
6
7
  from click import ClickException
7
- from snowflake.cli._plugins.nativeapp.artifacts import (
8
- BundleMap,
9
- resolve_without_follow,
10
- )
11
8
  from snowflake.cli._plugins.nativeapp.exceptions import (
12
9
  InvalidTemplateInFileError,
13
10
  MissingScriptError,
@@ -22,10 +19,11 @@ from snowflake.cli._plugins.stage.diff import (
22
19
  sync_local_diff_with_stage,
23
20
  to_stage_path,
24
21
  )
22
+ from snowflake.cli._plugins.stage.manager import DefaultStagePathParts
25
23
  from snowflake.cli._plugins.stage.utils import print_diff_to_console
26
- from snowflake.cli.api.cli_global_context import get_cli_context
24
+ from snowflake.cli.api.artifacts.bundle_map import BundleMap
25
+ from snowflake.cli.api.cli_global_context import get_cli_context, span
27
26
  from snowflake.cli.api.console.abc import AbstractConsole
28
- from snowflake.cli.api.entities.common import get_sql_executor
29
27
  from snowflake.cli.api.errno import (
30
28
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
31
29
  NO_WAREHOUSE_SELECTED_IN_SESSION,
@@ -41,8 +39,9 @@ from snowflake.cli.api.rendering.sql_templates import (
41
39
  choose_sql_jinja_env_based_on_template_syntax,
42
40
  )
43
41
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
42
+ from snowflake.cli.api.sql_execution import SqlExecutor
43
+ from snowflake.cli.api.utils.path_utils import resolve_without_follow
44
44
  from snowflake.connector import ProgrammingError
45
- from snowflake.connector.cursor import SnowflakeCursor
46
45
 
47
46
 
48
47
  def generic_sql_error_handler(err: ProgrammingError) -> NoReturn:
@@ -76,16 +75,16 @@ def _get_stage_paths_to_sync(
76
75
  return stage_paths
77
76
 
78
77
 
78
+ @span("sync_deploy_root_with_stage")
79
79
  def sync_deploy_root_with_stage(
80
80
  console: AbstractConsole,
81
81
  deploy_root: Path,
82
82
  package_name: str,
83
- stage_schema: str,
84
83
  bundle_map: BundleMap,
85
84
  role: str,
86
85
  prune: bool,
87
86
  recursive: bool,
88
- stage_fqn: str,
87
+ stage_path: DefaultStagePathParts,
89
88
  local_paths_to_sync: List[Path] | None = None,
90
89
  print_diff: bool = True,
91
90
  ) -> DiffResult:
@@ -98,32 +97,37 @@ def sync_deploy_root_with_stage(
98
97
  role (str): The name of the role to use for queries and commands.
99
98
  prune (bool): Whether to prune artifacts from the stage that don't exist locally.
100
99
  recursive (bool): Whether to traverse directories recursively.
101
- stage_fqn (str): The name of the stage to diff against and upload to.
100
+ stage_path (DefaultStagePathParts): stage path object.
101
+
102
102
  local_paths_to_sync (List[Path], optional): List of local paths to sync. Defaults to None to sync all
103
- local paths. Note that providing an empty list here is equivalent to None.
103
+ local paths. Note that providing an empty list here is equivalent to None.
104
104
  print_diff (bool): Whether to print the diff between the local files and the remote stage. Defaults to True
105
105
 
106
106
  Returns:
107
107
  A `DiffResult` instance describing the changes that were performed.
108
108
  """
109
-
110
109
  sql_facade = get_snowflake_facade()
110
+ schema = stage_path.schema
111
+ stage_fqn = stage_path.stage
111
112
  # Does a stage already exist within the application package, or we need to create one?
112
113
  # Using "if not exists" should take care of either case.
113
114
  console.step(
114
115
  f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
115
116
  )
116
117
  if not sql_facade.stage_exists(stage_fqn):
117
- sql_facade.create_schema(stage_schema, database=package_name)
118
+ sql_facade.create_schema(schema, database=package_name)
118
119
  sql_facade.create_stage(stage_fqn)
119
120
 
120
121
  # Perform a diff operation and display results to the user for informational purposes
121
122
  if print_diff:
122
123
  console.step(
123
- "Performing a diff between the Snowflake stage and your local deploy_root ('%s') directory."
124
- % deploy_root.resolve()
124
+ f"Performing a diff between the Snowflake stage: {stage_path.path} and your local deploy_root: {deploy_root.resolve()}."
125
125
  )
126
- diff: DiffResult = compute_stage_diff(deploy_root, stage_fqn)
126
+
127
+ diff: DiffResult = compute_stage_diff(
128
+ local_root=deploy_root,
129
+ stage_path=stage_path,
130
+ )
127
131
 
128
132
  if local_paths_to_sync:
129
133
  # Deploying specific files/directories
@@ -184,7 +188,7 @@ def sync_deploy_root_with_stage(
184
188
  role=role,
185
189
  deploy_root_path=deploy_root,
186
190
  diff_result=diff,
187
- stage_fqn=stage_fqn,
191
+ stage_full_path=stage_path.full_path,
188
192
  )
189
193
  return diff
190
194
 
@@ -212,7 +216,10 @@ def execute_post_deploy_hooks(
212
216
 
213
217
  get_cli_context().metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 1)
214
218
 
215
- with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
219
+ with (
220
+ console.phase(f"Executing {deployed_object_type} post-deploy actions"),
221
+ get_cli_context().metrics.span("post_deploy_hooks"),
222
+ ):
216
223
  sql_scripts_paths = []
217
224
  display_paths = []
218
225
  for hook in post_deploy_hooks:
@@ -321,17 +328,52 @@ def drop_generic_object(
321
328
  console.message(f"Dropped {object_type} {object_name} successfully.")
322
329
 
323
330
 
324
- def print_messages(
325
- console: AbstractConsole, create_or_upgrade_cursor: Optional[SnowflakeCursor]
326
- ):
331
+ def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]):
327
332
  """
328
333
  Shows messages in the console returned by the CREATE or UPGRADE
329
334
  APPLICATION command.
330
335
  """
331
- if not create_or_upgrade_cursor:
336
+ if not cursor_results:
332
337
  return
333
338
 
334
- messages = [row[0] for row in create_or_upgrade_cursor.fetchall()]
339
+ messages = [row[0] for row in cursor_results]
335
340
  for message in messages:
336
341
  console.warning(message)
337
342
  console.message("")
343
+
344
+
345
+ def get_sql_executor() -> SqlExecutor:
346
+ """Returns an SQL Executor that uses the connection from the current CLI context"""
347
+ return SqlExecutor()
348
+
349
+
350
+ class EntityActions(str, Enum):
351
+ BUNDLE = "action_bundle"
352
+ DEPLOY = "action_deploy"
353
+ DROP = "action_drop"
354
+ VALIDATE = "action_validate"
355
+ EVENTS = "action_events"
356
+ DIFF = "action_diff"
357
+
358
+ VERSION_LIST = "action_version_list"
359
+ VERSION_CREATE = "action_version_create"
360
+ VERSION_DROP = "action_version_drop"
361
+
362
+ RELEASE_DIRECTIVE_UNSET = "action_release_directive_unset"
363
+ RELEASE_DIRECTIVE_SET = "action_release_directive_set"
364
+ RELEASE_DIRECTIVE_LIST = "action_release_directive_list"
365
+ RELEASE_DIRECTIVE_ADD_ACCOUNTS = "action_release_directive_add_accounts"
366
+ RELEASE_DIRECTIVE_REMOVE_ACCOUNTS = "action_release_directive_remove_accounts"
367
+
368
+ RELEASE_CHANNEL_LIST = "action_release_channel_list"
369
+ RELEASE_CHANNEL_ADD_ACCOUNTS = "action_release_channel_add_accounts"
370
+ RELEASE_CHANNEL_REMOVE_ACCOUNTS = "action_release_channel_remove_accounts"
371
+ RELEASE_CHANNEL_ADD_VERSION = "action_release_channel_add_version"
372
+ RELEASE_CHANNEL_REMOVE_VERSION = "action_release_channel_remove_version"
373
+ RELEASE_CHANNEL_SET_ACCOUNTS = "action_release_channel_set_accounts"
374
+
375
+ PUBLISH = "action_publish"
376
+
377
+ @property
378
+ def get_action_name(self):
379
+ return self.value.replace("action_", "")