snowflake-cli 3.3.0__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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +224 -192
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/commands.py +21 -19
- snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
- snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
- snowflake/cli/_plugins/notebook/commands.py +55 -2
- snowflake/cli/_plugins/notebook/exceptions.py +1 -1
- snowflake/cli/_plugins/notebook/manager.py +3 -3
- snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
- snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
- snowflake/cli/_plugins/notebook/types.py +3 -0
- snowflake/cli/_plugins/snowpark/commands.py +48 -30
- snowflake/cli/_plugins/snowpark/common.py +47 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
- snowflake/cli/_plugins/snowpark/zipper.py +33 -1
- snowflake/cli/_plugins/spcs/services/commands.py +0 -3
- snowflake/cli/_plugins/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +24 -11
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +10 -1
- snowflake/cli/_plugins/streamlit/manager.py +62 -21
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
- snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
- snowflake/cli/_plugins/workspace/commands.py +3 -3
- snowflake/cli/_plugins/workspace/manager.py +1 -1
- snowflake/cli/api/artifacts/__init__.py +13 -0
- snowflake/cli/api/artifacts/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +14 -1
- snowflake/cli/api/commands/flags.py +10 -4
- snowflake/cli/api/commands/utils.py +28 -2
- snowflake/cli/api/constants.py +1 -0
- snowflake/cli/api/entities/common.py +14 -32
- snowflake/cli/api/entities/resolver.py +160 -0
- snowflake/cli/api/entities/utils.py +56 -15
- snowflake/cli/api/errno.py +3 -0
- snowflake/cli/api/feature_flags.py +1 -2
- snowflake/cli/api/project/definition_conversion.py +3 -2
- snowflake/cli/api/project/project_paths.py +28 -0
- snowflake/cli/api/project/schemas/entities/common.py +130 -1
- snowflake/cli/api/project/schemas/entities/entities.py +4 -0
- snowflake/cli/api/project/schemas/project_definition.py +27 -0
- snowflake/cli/api/project/schemas/updatable_model.py +2 -2
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
- snowflake/cli/api/secure_path.py +6 -0
- snowflake/cli/api/sql_execution.py +5 -1
- snowflake/cli/api/stage_path.py +7 -2
- snowflake/cli/api/utils/graph.py +3 -0
- snowflake/cli/api/utils/path_utils.py +24 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +8 -9
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +76 -67
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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
|
|
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]
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -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")
|
|
@@ -1,40 +1,16 @@
|
|
|
1
1
|
import functools
|
|
2
|
-
from enum import Enum
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from typing import Generic, Type, TypeVar, get_args
|
|
5
4
|
|
|
6
5
|
from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
|
|
7
|
-
from snowflake.cli.api.cli_global_context import span
|
|
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
|
|
8
9
|
from snowflake.cli.api.identifiers import FQN
|
|
9
10
|
from snowflake.cli.api.sql_execution import SqlExecutor
|
|
10
11
|
from snowflake.connector import SnowflakeConnection
|
|
11
12
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
class EntityActions(str, Enum):
|
|
15
|
-
BUNDLE = "action_bundle"
|
|
16
|
-
DEPLOY = "action_deploy"
|
|
17
|
-
DROP = "action_drop"
|
|
18
|
-
VALIDATE = "action_validate"
|
|
19
|
-
EVENTS = "action_events"
|
|
20
|
-
|
|
21
|
-
VERSION_LIST = "action_version_list"
|
|
22
|
-
VERSION_CREATE = "action_version_create"
|
|
23
|
-
VERSION_DROP = "action_version_drop"
|
|
24
|
-
|
|
25
|
-
RELEASE_DIRECTIVE_UNSET = "action_release_directive_unset"
|
|
26
|
-
RELEASE_DIRECTIVE_SET = "action_release_directive_set"
|
|
27
|
-
RELEASE_DIRECTIVE_LIST = "action_release_directive_list"
|
|
28
|
-
|
|
29
|
-
RELEASE_CHANNEL_LIST = "action_release_channel_list"
|
|
30
|
-
RELEASE_CHANNEL_ADD_ACCOUNTS = "action_release_channel_add_accounts"
|
|
31
|
-
RELEASE_CHANNEL_REMOVE_ACCOUNTS = "action_release_channel_remove_accounts"
|
|
32
|
-
RELEASE_CHANNEL_ADD_VERSION = "action_release_channel_add_version"
|
|
33
|
-
RELEASE_CHANNEL_REMOVE_VERSION = "action_release_channel_remove_version"
|
|
34
|
-
|
|
35
|
-
PUBLISH = "action_publish"
|
|
36
|
-
|
|
37
|
-
|
|
38
14
|
T = TypeVar("T")
|
|
39
15
|
|
|
40
16
|
|
|
@@ -71,6 +47,7 @@ class EntityBase(Generic[T]):
|
|
|
71
47
|
def __init__(self, entity_model: T, workspace_ctx: WorkspaceContext):
|
|
72
48
|
self._entity_model = entity_model
|
|
73
49
|
self._workspace_ctx = workspace_ctx
|
|
50
|
+
self.dependency_resolver = DependencyResolver(entity_model)
|
|
74
51
|
|
|
75
52
|
@property
|
|
76
53
|
def entity_id(self) -> str:
|
|
@@ -96,7 +73,10 @@ class EntityBase(Generic[T]):
|
|
|
96
73
|
):
|
|
97
74
|
"""
|
|
98
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.
|
|
99
78
|
"""
|
|
79
|
+
self.dependency_resolver.perform_for_dep(action, action_ctx, *args, **kwargs)
|
|
100
80
|
return getattr(self, action)(action_ctx, *args, **kwargs)
|
|
101
81
|
|
|
102
82
|
@property
|
|
@@ -124,10 +104,17 @@ class EntityBase(Generic[T]):
|
|
|
124
104
|
def _conn(self) -> SnowflakeConnection:
|
|
125
105
|
return self._sql_executor._conn # noqa
|
|
126
106
|
|
|
107
|
+
@property
|
|
108
|
+
def snow_api_root(self):
|
|
109
|
+
return get_cli_context().snow_api_root
|
|
110
|
+
|
|
127
111
|
@property
|
|
128
112
|
def model(self):
|
|
129
113
|
return self._entity_model
|
|
130
114
|
|
|
115
|
+
def dependent_entities(self, action_ctx: ActionContext):
|
|
116
|
+
return self.dependency_resolver.depends_on(action_ctx)
|
|
117
|
+
|
|
131
118
|
def get_usage_grant_sql(self, app_role: str) -> str:
|
|
132
119
|
return f"GRANT USAGE ON {self.model.type.upper()} {self.identifier} TO ROLE {app_role};"
|
|
133
120
|
|
|
@@ -136,8 +123,3 @@ class EntityBase(Generic[T]):
|
|
|
136
123
|
|
|
137
124
|
def get_drop_sql(self) -> str:
|
|
138
125
|
return f"DROP {self.model.type.upper()} {self.identifier};"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def get_sql_executor() -> SqlExecutor:
|
|
142
|
-
"""Returns an SQL Executor that uses the connection from the current CLI context"""
|
|
143
|
-
return SqlExecutor()
|
|
@@ -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
|
|
24
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
26
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,6 +39,8 @@ 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
45
|
|
|
46
46
|
|
|
@@ -80,12 +80,11 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
stage_full_path=stage_path.full_path,
|
|
188
192
|
)
|
|
189
193
|
return diff
|
|
190
194
|
|
|
@@ -336,3 +340,40 @@ def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]):
|
|
|
336
340
|
for message in messages:
|
|
337
341
|
console.warning(message)
|
|
338
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_", "")
|
snowflake/cli/api/errno.py
CHANGED
|
@@ -54,6 +54,7 @@ APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148
|
|
|
54
54
|
CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118
|
|
55
55
|
APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
|
|
56
56
|
APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168
|
|
57
|
+
CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER = 93167
|
|
57
58
|
APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197
|
|
58
59
|
NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301
|
|
59
60
|
NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302
|
|
@@ -64,8 +65,10 @@ CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
|
|
|
64
65
|
VERSION_NOT_ADDED_TO_RELEASE_CHANNEL = 512008
|
|
65
66
|
CANNOT_DISABLE_RELEASE_CHANNELS = 512001
|
|
66
67
|
RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND = 93036
|
|
68
|
+
RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH = 93074
|
|
67
69
|
RELEASE_DIRECTIVE_DOES_NOT_EXIST = 93090
|
|
68
70
|
VERSION_DOES_NOT_EXIST = 93031
|
|
71
|
+
CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH = 93170
|
|
69
72
|
VERSION_NOT_IN_RELEASE_CHANNEL = 512010
|
|
70
73
|
ACCOUNT_DOES_NOT_EXIST = 1999
|
|
71
74
|
ACCOUNT_HAS_TOO_MANY_QUALIFIERS = 906
|
|
@@ -66,5 +66,4 @@ class FeatureFlag(FeatureFlagMixin):
|
|
|
66
66
|
ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
|
|
67
67
|
"ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
|
|
68
68
|
)
|
|
69
|
-
|
|
70
|
-
ENABLE_SPCS_SERVICE_METRICS = BooleanFlag("ENABLE_SPCS_SERVICE_METRICS", False)
|
|
69
|
+
ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
|
|
@@ -222,10 +222,11 @@ def convert_streamlit_to_v2_data(streamlit: Streamlit) -> Dict[str, Any]:
|
|
|
222
222
|
environment_file,
|
|
223
223
|
pages_dir,
|
|
224
224
|
]
|
|
225
|
-
artifacts = [a for a in artifacts if a is not None]
|
|
225
|
+
artifacts = [str(a) for a in artifacts if a is not None]
|
|
226
226
|
|
|
227
227
|
if streamlit.additional_source_files:
|
|
228
|
-
|
|
228
|
+
for additional_file in streamlit.additional_source_files:
|
|
229
|
+
artifacts.append(str(additional_file))
|
|
229
230
|
|
|
230
231
|
identifier = {"name": streamlit.name}
|
|
231
232
|
if streamlit.schema_name:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ProjectPaths:
|
|
9
|
+
"""
|
|
10
|
+
This class allows you to manage files paths related to given project.
|
|
11
|
+
Class provides bundle root path and allows to remove it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
project_root: Path
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def bundle_root(self) -> Path:
|
|
18
|
+
return bundle_root(self.project_root)
|
|
19
|
+
|
|
20
|
+
def remove_up_bundle_root(self) -> None:
|
|
21
|
+
if self.bundle_root.exists():
|
|
22
|
+
SecurePath(self.bundle_root).rmdir(recursive=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def bundle_root(root: Path, app_type: str | None = None) -> Path:
|
|
26
|
+
if app_type:
|
|
27
|
+
return root / "output" / "bundle" / app_type
|
|
28
|
+
return root / "output" / "bundle"
|