snowflake-cli 3.3.0__py3-none-any.whl → 3.5.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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +220 -197
- snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +4 -30
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_plugins/connection/commands.py +2 -4
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- 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 +54 -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/plugin/commands.py +79 -0
- snowflake/cli/_plugins/plugin/manager.py +74 -0
- snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/__init__.py +0 -0
- snowflake/cli/_plugins/project/commands.py +157 -0
- snowflake/cli/_plugins/project/feature_flags.py +22 -0
- snowflake/cli/_plugins/project/manager.py +76 -0
- snowflake/cli/_plugins/project/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/project_entity_model.py +40 -0
- snowflake/cli/_plugins/snowpark/commands.py +49 -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/compute_pool/commands.py +53 -5
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +51 -1
- snowflake/cli/_plugins/spcs/services/manager.py +114 -0
- snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
- snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
- snowflake/cli/_plugins/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +26 -13
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +18 -24
- snowflake/cli/_plugins/streamlit/manager.py +37 -27
- 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/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +14 -1
- snowflake/cli/api/commands/flags.py +34 -13
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +30 -2
- snowflake/cli/api/config.py +15 -10
- 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/exceptions.py +8 -1
- snowflake/cli/api/feature_flags.py +1 -1
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_conversion.py +3 -2
- snowflake/cli/api/project/definition_helper.py +31 -0
- 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 +30 -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.5.0.dist-info}/METADATA +12 -13
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +109 -85
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- /snowflake/cli/{_app/api_impl/plugin → api/artifacts}/__init__.py +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
snowflake/cli/api/exceptions.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
18
|
+
from typing import List, Optional
|
|
19
19
|
|
|
20
20
|
from click.exceptions import ClickException, UsageError
|
|
21
21
|
from snowflake.cli.api.constants import ObjectType
|
|
@@ -54,6 +54,13 @@ class InvalidPluginConfiguration(ClickException):
|
|
|
54
54
|
return f"Invalid plugin configuration. {self.message}"
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
class PluginNotInstalledError(ClickException):
|
|
58
|
+
def __init__(self, plugin_name, installed_plugins: List[str]):
|
|
59
|
+
super().__init__(
|
|
60
|
+
f"Plugin {plugin_name} is not installed. Available plugins: {', '.join(installed_plugins)}."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
57
64
|
class SnowflakeConnectionError(ClickException):
|
|
58
65
|
def __init__(self, snowflake_err: Exception):
|
|
59
66
|
super().__init__(f"Could not connect to Snowflake. Reason: {snowflake_err}")
|
|
@@ -66,5 +66,5 @@ class FeatureFlag(FeatureFlagMixin):
|
|
|
66
66
|
ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
|
|
67
67
|
"ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
|
|
68
68
|
)
|
|
69
|
+
ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
|
|
69
70
|
ENABLE_SPCS_SERVICE_EVENTS = BooleanFlag("ENABLE_SPCS_SERVICE_EVENTS", False)
|
|
70
|
-
ENABLE_SPCS_SERVICE_METRICS = BooleanFlag("ENABLE_SPCS_SERVICE_METRICS", False)
|
|
@@ -17,6 +17,16 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from typing import Any, Dict, List
|
|
19
19
|
|
|
20
|
+
from snowflake.cli.api.config import (
|
|
21
|
+
PLUGIN_ENABLED_KEY,
|
|
22
|
+
PLUGINS_SECTION_PATH,
|
|
23
|
+
config_section_exists,
|
|
24
|
+
get_config_section,
|
|
25
|
+
get_config_value,
|
|
26
|
+
get_plugins_config,
|
|
27
|
+
)
|
|
28
|
+
from snowflake.cli.api.exceptions import InvalidPluginConfiguration
|
|
29
|
+
|
|
20
30
|
|
|
21
31
|
@dataclass
|
|
22
32
|
class PluginConfig:
|
|
@@ -25,8 +35,37 @@ class PluginConfig:
|
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
class PluginConfigProvider:
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_enabled_plugin_names() -> List[str]:
|
|
40
|
+
enabled_plugins = []
|
|
41
|
+
for plugin_name, plugin_config_section in get_plugins_config().items():
|
|
42
|
+
enabled = plugin_config_section.get(PLUGIN_ENABLED_KEY, False)
|
|
43
|
+
_assert_value_is_bool(
|
|
44
|
+
enabled, value_name=PLUGIN_ENABLED_KEY, plugin_name=plugin_name
|
|
45
|
+
)
|
|
46
|
+
if enabled:
|
|
47
|
+
enabled_plugins.append(plugin_name)
|
|
48
|
+
return enabled_plugins
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def get_config(plugin_name: str) -> PluginConfig:
|
|
52
|
+
config_path = PLUGINS_SECTION_PATH + [plugin_name]
|
|
53
|
+
plugin_config = PluginConfig(is_plugin_enabled=False, internal_config={})
|
|
54
|
+
plugin_config.is_plugin_enabled = get_config_value(
|
|
55
|
+
*config_path, key=PLUGIN_ENABLED_KEY, default=False
|
|
56
|
+
)
|
|
57
|
+
_assert_value_is_bool(
|
|
58
|
+
plugin_config.is_plugin_enabled,
|
|
59
|
+
value_name=PLUGIN_ENABLED_KEY,
|
|
60
|
+
plugin_name=plugin_name,
|
|
61
|
+
)
|
|
62
|
+
if config_section_exists(*config_path, "config"):
|
|
63
|
+
plugin_config.internal_config = get_config_section(*config_path, "config")
|
|
64
|
+
return plugin_config
|
|
65
|
+
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
def _assert_value_is_bool(value, *, value_name: str, plugin_name: str) -> None:
|
|
68
|
+
if type(value) is not bool:
|
|
69
|
+
raise InvalidPluginConfiguration(
|
|
70
|
+
f'[{plugin_name}]: "{value_name}" must be a boolean'
|
|
71
|
+
)
|
|
@@ -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,31 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from click import UsageError
|
|
4
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
5
|
+
from snowflake.cli.api.constants import ObjectType
|
|
6
|
+
from snowflake.cli.api.exceptions import NoProjectDefinitionError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_entity_from_project_definition(
|
|
10
|
+
entity_type: ObjectType, entity_id: Optional[str] = None
|
|
11
|
+
):
|
|
12
|
+
cli_context = get_cli_context()
|
|
13
|
+
pd = cli_context.project_definition
|
|
14
|
+
entities = pd.get_entities_by_type(entity_type=entity_type.value.cli_name)
|
|
15
|
+
|
|
16
|
+
if not entities:
|
|
17
|
+
raise NoProjectDefinitionError(
|
|
18
|
+
project_type=entity_type.value.sf_name,
|
|
19
|
+
project_root=cli_context.project_root,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if entity_id and entity_id not in entities:
|
|
23
|
+
raise UsageError(f"No '{entity_id}' entity in project definition file.")
|
|
24
|
+
elif len(entities.keys()) == 1:
|
|
25
|
+
entity_id = list(entities.keys())[0]
|
|
26
|
+
|
|
27
|
+
if entity_id is None:
|
|
28
|
+
raise UsageError(
|
|
29
|
+
f"Multiple {entity_type.value.sf_plural_name} found. Please provide entity id for the operation."
|
|
30
|
+
)
|
|
31
|
+
return entities[entity_id]
|
|
@@ -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"
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from abc import ABC
|
|
18
|
-
from typing import Dict, Generic, List, Optional, TypeVar, Union
|
|
18
|
+
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
|
|
19
19
|
|
|
20
20
|
from pydantic import Field, PrivateAttr, field_validator
|
|
21
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
21
22
|
from snowflake.cli.api.identifiers import FQN
|
|
22
23
|
from snowflake.cli.api.project.schemas.updatable_model import (
|
|
23
24
|
IdentifierField,
|
|
@@ -61,6 +62,15 @@ class MetaField(UpdatableModel):
|
|
|
61
62
|
default=None,
|
|
62
63
|
)
|
|
63
64
|
|
|
65
|
+
depends_on: Optional[List[str]] = Field(
|
|
66
|
+
title="Entities that need to be deployed before this one", default_factory=list
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
action_arguments: Optional[Dict[str, Dict[str, Union[int, bool, str]]]] = Field(
|
|
70
|
+
title="Arguments that will be used, when this entity is called as a dependency of other entity",
|
|
71
|
+
default_factory=dict,
|
|
72
|
+
)
|
|
73
|
+
|
|
64
74
|
@field_validator("use_mixins", mode="before")
|
|
65
75
|
@classmethod
|
|
66
76
|
def ensure_use_mixins_is_a_list(
|
|
@@ -70,6 +80,35 @@ class MetaField(UpdatableModel):
|
|
|
70
80
|
return [mixins]
|
|
71
81
|
return mixins
|
|
72
82
|
|
|
83
|
+
@field_validator("action_arguments", mode="before")
|
|
84
|
+
@classmethod
|
|
85
|
+
def arguments_validator(cls, arguments: Dict, info: ValidationInfo) -> Dict:
|
|
86
|
+
duplicated_run = (
|
|
87
|
+
info.context.get("is_duplicated_run", False) if info.context else False
|
|
88
|
+
)
|
|
89
|
+
if not duplicated_run:
|
|
90
|
+
for argument_dict in arguments.values():
|
|
91
|
+
for k, v in argument_dict.items():
|
|
92
|
+
argument_dict[k] = cls._cast_value(v)
|
|
93
|
+
|
|
94
|
+
return arguments
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _cast_value(value: str) -> Union[int, bool, str]:
|
|
98
|
+
if value.lower() in ["true", "false"]:
|
|
99
|
+
return value.lower() == "true"
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return int(value)
|
|
103
|
+
except ValueError:
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
def __eq__(self, other):
|
|
107
|
+
return self.entity_id == other.entity_id
|
|
108
|
+
|
|
109
|
+
def __hash__(self):
|
|
110
|
+
return hash(self.entity_id)
|
|
111
|
+
|
|
73
112
|
|
|
74
113
|
class Identifier(UpdatableModel):
|
|
75
114
|
name: str = Field(title="Entity name")
|
|
@@ -141,6 +180,23 @@ class ImportsBaseModel:
|
|
|
141
180
|
return f"IMPORTS = ({imports})"
|
|
142
181
|
|
|
143
182
|
|
|
183
|
+
class Grant(UpdatableModel):
|
|
184
|
+
privilege: str = Field(title="Required privileges")
|
|
185
|
+
role: str = Field(title="Role to which the privileges will be granted")
|
|
186
|
+
|
|
187
|
+
def get_grant_sql(self, entity_model: EntityModelBase) -> str:
|
|
188
|
+
return f"GRANT {self.privilege} ON {entity_model.get_type().upper()} {entity_model.fqn.sql_identifier} TO ROLE {self.role}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class GrantBaseModel(UpdatableModel):
|
|
192
|
+
grants: Optional[List[Grant]] = Field(title="List of grants", default=None)
|
|
193
|
+
|
|
194
|
+
def get_grant_sqls(self) -> list[str]:
|
|
195
|
+
return (
|
|
196
|
+
[grant.get_grant_sql(self) for grant in self.grants] if self.grants else []
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
144
200
|
class ExternalAccessBaseModel:
|
|
145
201
|
external_access_integrations: Optional[List[str]] = Field(
|
|
146
202
|
title="Names of external access integrations needed for this entity to access external networks",
|
|
@@ -162,3 +218,76 @@ class ExternalAccessBaseModel:
|
|
|
162
218
|
return None
|
|
163
219
|
secrets = ", ".join(f"'{key}'={value}" for key, value in self.secrets.items())
|
|
164
220
|
return f"secrets=({secrets})"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ProcessorMapping(UpdatableModel):
|
|
224
|
+
name: str = Field(
|
|
225
|
+
title="Name of a processor to invoke on a collection of artifacts."
|
|
226
|
+
)
|
|
227
|
+
properties: Optional[Dict[str, Any]] = Field(
|
|
228
|
+
title="A set of key-value pairs used to configure the output of the processor. Consult a specific processor's documentation for more details on the supported properties.",
|
|
229
|
+
default=None,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class PathMapping(UpdatableModel):
|
|
234
|
+
src: str = Field(
|
|
235
|
+
title="Source path or glob pattern (relative to project root)", default=None
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
dest: Optional[str] = Field(
|
|
239
|
+
title="Destination path on stage",
|
|
240
|
+
description="Paths are relative to stage root; paths ending with a slash indicate that the destination is a directory which source files should be copied into.",
|
|
241
|
+
default=None,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
processors: Optional[List[Union[str, ProcessorMapping]]] = Field(
|
|
245
|
+
title="List of processors to apply to matching source files during bundling.",
|
|
246
|
+
default=[],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@field_validator("processors")
|
|
250
|
+
@classmethod
|
|
251
|
+
def transform_processors(
|
|
252
|
+
cls, input_values: Optional[List[Union[str, Dict, ProcessorMapping]]]
|
|
253
|
+
) -> List[ProcessorMapping]:
|
|
254
|
+
if input_values is None:
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
transformed_processors: List[ProcessorMapping] = []
|
|
258
|
+
for input_processor in input_values:
|
|
259
|
+
if isinstance(input_processor, str):
|
|
260
|
+
transformed_processors.append(ProcessorMapping(name=input_processor))
|
|
261
|
+
elif isinstance(input_processor, Dict):
|
|
262
|
+
transformed_processors.append(ProcessorMapping(**input_processor))
|
|
263
|
+
else:
|
|
264
|
+
transformed_processors.append(input_processor)
|
|
265
|
+
return transformed_processors
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
Artifacts = List[Union[PathMapping, str]]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class EntityModelBaseWithArtifacts(EntityModelBase):
|
|
272
|
+
artifacts: Artifacts = Field(
|
|
273
|
+
title="List of paths or file source/destination pairs to add to the deploy root",
|
|
274
|
+
)
|
|
275
|
+
deploy_root: Optional[str] = Field(
|
|
276
|
+
title="Folder at the root of your project where the build step copies the artifacts",
|
|
277
|
+
default="output/deploy/",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
@field_validator("artifacts")
|
|
281
|
+
@classmethod
|
|
282
|
+
def transform_artifacts(cls, orig_artifacts: Artifacts) -> List[PathMapping]:
|
|
283
|
+
transformed_artifacts: List[PathMapping] = []
|
|
284
|
+
if orig_artifacts is None:
|
|
285
|
+
return transformed_artifacts
|
|
286
|
+
|
|
287
|
+
for artifact in orig_artifacts:
|
|
288
|
+
if isinstance(artifact, PathMapping):
|
|
289
|
+
transformed_artifacts.append(artifact)
|
|
290
|
+
else:
|
|
291
|
+
transformed_artifacts.append(PathMapping(src=artifact))
|
|
292
|
+
|
|
293
|
+
return transformed_artifacts
|
|
@@ -24,6 +24,12 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import (
|
|
|
24
24
|
ApplicationPackageEntity,
|
|
25
25
|
ApplicationPackageEntityModel,
|
|
26
26
|
)
|
|
27
|
+
from snowflake.cli._plugins.notebook.notebook_entity import NotebookEntity
|
|
28
|
+
from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntityModel
|
|
29
|
+
from snowflake.cli._plugins.project.project_entity_model import (
|
|
30
|
+
ProjectEntity,
|
|
31
|
+
ProjectEntityModel,
|
|
32
|
+
)
|
|
27
33
|
from snowflake.cli._plugins.snowpark.snowpark_entity import (
|
|
28
34
|
FunctionEntity,
|
|
29
35
|
ProcedureEntity,
|
|
@@ -32,6 +38,20 @@ from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
|
|
|
32
38
|
FunctionEntityModel,
|
|
33
39
|
ProcedureEntityModel,
|
|
34
40
|
)
|
|
41
|
+
from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity import (
|
|
42
|
+
ComputePoolEntity,
|
|
43
|
+
)
|
|
44
|
+
from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity_model import (
|
|
45
|
+
ComputePoolEntityModel,
|
|
46
|
+
)
|
|
47
|
+
from snowflake.cli._plugins.spcs.image_repository.image_repository_entity import (
|
|
48
|
+
ImageRepositoryEntity,
|
|
49
|
+
)
|
|
50
|
+
from snowflake.cli._plugins.spcs.image_repository.image_repository_entity_model import (
|
|
51
|
+
ImageRepositoryEntityModel,
|
|
52
|
+
)
|
|
53
|
+
from snowflake.cli._plugins.spcs.services.service_entity import ServiceEntity
|
|
54
|
+
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
|
|
35
55
|
from snowflake.cli._plugins.streamlit.streamlit_entity import StreamlitEntity
|
|
36
56
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
37
57
|
StreamlitEntityModel,
|
|
@@ -42,7 +62,12 @@ Entity = Union[
|
|
|
42
62
|
ApplicationPackageEntity,
|
|
43
63
|
StreamlitEntity,
|
|
44
64
|
ProcedureEntity,
|
|
65
|
+
ProjectEntity,
|
|
45
66
|
FunctionEntity,
|
|
67
|
+
ComputePoolEntity,
|
|
68
|
+
ImageRepositoryEntity,
|
|
69
|
+
ServiceEntity,
|
|
70
|
+
NotebookEntity,
|
|
46
71
|
]
|
|
47
72
|
EntityModel = Union[
|
|
48
73
|
ApplicationEntityModel,
|
|
@@ -50,6 +75,11 @@ EntityModel = Union[
|
|
|
50
75
|
StreamlitEntityModel,
|
|
51
76
|
FunctionEntityModel,
|
|
52
77
|
ProcedureEntityModel,
|
|
78
|
+
ComputePoolEntityModel,
|
|
79
|
+
ImageRepositoryEntityModel,
|
|
80
|
+
ServiceEntityModel,
|
|
81
|
+
NotebookEntityModel,
|
|
82
|
+
ProjectEntityModel,
|
|
53
83
|
]
|
|
54
84
|
|
|
55
85
|
ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from collections import defaultdict
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from types import UnionType
|
|
19
20
|
from typing import Any, Dict, List, Optional, Union, get_args, get_origin
|
|
@@ -262,6 +263,21 @@ class DefinitionV20(_ProjectDefinitionBase):
|
|
|
262
263
|
data = cls._merge_data(data, entity)
|
|
263
264
|
return data
|
|
264
265
|
|
|
266
|
+
@model_validator(mode="after")
|
|
267
|
+
def validate_dependencies(self):
|
|
268
|
+
"""
|
|
269
|
+
Checks if entities listed in depends_on section exist in the project
|
|
270
|
+
"""
|
|
271
|
+
missing_dependencies = defaultdict(list)
|
|
272
|
+
for entity_id, entity in self.entities.items():
|
|
273
|
+
if entity.meta:
|
|
274
|
+
for dependency in entity.meta.depends_on:
|
|
275
|
+
if dependency not in self.entities:
|
|
276
|
+
missing_dependencies[entity_id].append(dependency)
|
|
277
|
+
|
|
278
|
+
if missing_dependencies:
|
|
279
|
+
raise ValueError(_get_missing_dependencies_message(missing_dependencies))
|
|
280
|
+
|
|
265
281
|
@classmethod
|
|
266
282
|
def _merge_data(
|
|
267
283
|
cls,
|
|
@@ -354,3 +370,14 @@ def _unique_extend(list_a: List, list_b: List) -> List:
|
|
|
354
370
|
if all(item != x for x in list_a):
|
|
355
371
|
new_list.append(item)
|
|
356
372
|
return new_list
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _get_missing_dependencies_message(
|
|
376
|
+
missing_dependencies: Dict[str, List[str]]
|
|
377
|
+
) -> str:
|
|
378
|
+
missing_dependencies_message = []
|
|
379
|
+
for entity_id, dependencies in missing_dependencies.items():
|
|
380
|
+
missing_dependencies_message.append(
|
|
381
|
+
f"\n Entity {entity_id} depends on non-existing entities: {', '.join(dependencies)}"
|
|
382
|
+
)
|
|
383
|
+
return "".join(missing_dependencies_message)
|
|
@@ -122,10 +122,10 @@ class UpdatableModel(BaseModel):
|
|
|
122
122
|
class_dict = class_.__dict__
|
|
123
123
|
field_annotations.update(class_dict.get("__annotations__", {}))
|
|
124
124
|
|
|
125
|
-
if "model_fields" in class_dict:
|
|
125
|
+
if "model_fields" in class_dict and class_.model_fields:
|
|
126
126
|
# This means the class dict has already been processed by Pydantic
|
|
127
127
|
# All fields should properly be populated in model_fields
|
|
128
|
-
field_values.update(
|
|
128
|
+
field_values.update(class_.model_fields)
|
|
129
129
|
else:
|
|
130
130
|
# If Pydantic did not process this class yet, get the values from class_dict directly
|
|
131
131
|
field_values.update(class_dict)
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
|
-
from typing import List, Optional
|
|
18
|
+
from typing import List, Optional
|
|
19
19
|
|
|
20
20
|
from pydantic import Field, field_validator
|
|
21
|
+
from snowflake.cli.api.project.schemas.entities.common import Artifacts, PathMapping
|
|
21
22
|
from snowflake.cli.api.project.schemas.updatable_model import UpdatableModel
|
|
22
23
|
from snowflake.cli.api.project.schemas.v1.native_app.application import (
|
|
23
24
|
Application,
|
|
24
25
|
ApplicationV11,
|
|
25
26
|
)
|
|
26
27
|
from snowflake.cli.api.project.schemas.v1.native_app.package import Package, PackageV11
|
|
27
|
-
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
|
|
28
28
|
from snowflake.cli.api.project.util import (
|
|
29
29
|
SCHEMA_AND_NAME,
|
|
30
30
|
)
|
|
@@ -34,7 +34,7 @@ class NativeApp(UpdatableModel):
|
|
|
34
34
|
name: str = Field(
|
|
35
35
|
title="Project identifier",
|
|
36
36
|
)
|
|
37
|
-
artifacts:
|
|
37
|
+
artifacts: Artifacts = Field(
|
|
38
38
|
title="List of file source and destination pairs to add to the deploy root",
|
|
39
39
|
)
|
|
40
40
|
bundle_root: Optional[str] = Field(
|
|
@@ -69,10 +69,8 @@ class NativeApp(UpdatableModel):
|
|
|
69
69
|
|
|
70
70
|
@field_validator("artifacts")
|
|
71
71
|
@classmethod
|
|
72
|
-
def transform_artifacts(
|
|
73
|
-
|
|
74
|
-
) -> List[PathMapping]:
|
|
75
|
-
transformed_artifacts = []
|
|
72
|
+
def transform_artifacts(cls, orig_artifacts: Artifacts) -> List[PathMapping]:
|
|
73
|
+
transformed_artifacts: List[PathMapping] = []
|
|
76
74
|
if orig_artifacts is None:
|
|
77
75
|
return transformed_artifacts
|
|
78
76
|
|
snowflake/cli/api/secure_path.py
CHANGED
|
@@ -104,6 +104,12 @@ class SecurePath:
|
|
|
104
104
|
"""
|
|
105
105
|
return self._path.is_file()
|
|
106
106
|
|
|
107
|
+
def glob(self, pattern: str):
|
|
108
|
+
"""
|
|
109
|
+
Return a generator yielding Path objects that match the given pattern.
|
|
110
|
+
"""
|
|
111
|
+
return self._path.glob(pattern)
|
|
112
|
+
|
|
107
113
|
@property
|
|
108
114
|
def name(self) -> str:
|
|
109
115
|
"""A string representing the final path component."""
|
|
@@ -92,7 +92,11 @@ class BaseSqlExecutor:
|
|
|
92
92
|
|
|
93
93
|
def execute_queries(self, queries: str, **kwargs):
|
|
94
94
|
"""Executes multiple SQL queries (passed as one string) and returns the results as a list"""
|
|
95
|
-
|
|
95
|
+
|
|
96
|
+
# Without remove_comments=True, connectors might throw an error if there is a comment at the end of the file
|
|
97
|
+
return list(
|
|
98
|
+
self._execute_string(dedent(queries), remove_comments=True, **kwargs)
|
|
99
|
+
)
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class SqlExecutor(BaseSqlExecutor):
|