snowflake-cli 3.2.1__py3-none-any.whl → 3.3.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/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
- snowflake/cli/_plugins/nativeapp/commands.py +92 -2
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
- snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
- snowflake/cli/_plugins/notebook/manager.py +4 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +134 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/manager.py +12 -4
- snowflake/cli/_plugins/streamlit/manager.py +8 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
- snowflake/cli/_plugins/workspace/commands.py +3 -2
- snowflake/cli/_plugins/workspace/manager.py +8 -4
- snowflake/cli/api/cli_global_context.py +22 -1
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +9 -1
- snowflake/cli/api/entities/common.py +85 -0
- snowflake/cli/api/entities/utils.py +9 -8
- snowflake/cli/api/errno.py +60 -3
- snowflake/cli/api/feature_flags.py +20 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +1 -2
- snowflake/cli/api/project/schemas/project_definition.py +27 -6
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- snowflake/cli/api/rest_api.py +3 -2
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,6 +17,7 @@ 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
22
|
from typing import TYPE_CHECKING, Iterator
|
|
22
23
|
|
|
@@ -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
|
|
@@ -213,6 +214,26 @@ def get_cli_context() -> _CliGlobalContextAccess:
|
|
|
213
214
|
return _CliGlobalContextAccess(get_cli_context_manager())
|
|
214
215
|
|
|
215
216
|
|
|
217
|
+
def span(span_name: str):
|
|
218
|
+
"""
|
|
219
|
+
Decorator to start a command metrics span that encompasses a whole function
|
|
220
|
+
|
|
221
|
+
Must be used instead of directly calling @get_cli_context().metrics.span(span_name)
|
|
222
|
+
as a decorator to ensure that the cli context is grabbed at run time instead of at
|
|
223
|
+
module load time, which would not reflect forking
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def decorator(func):
|
|
227
|
+
@wraps(func)
|
|
228
|
+
def wrapper(*args, **kwargs):
|
|
229
|
+
with get_cli_context().metrics.span(span_name):
|
|
230
|
+
return func(*args, **kwargs)
|
|
231
|
+
|
|
232
|
+
return wrapper
|
|
233
|
+
|
|
234
|
+
return decorator
|
|
235
|
+
|
|
236
|
+
|
|
216
237
|
@contextmanager
|
|
217
238
|
def fork_cli_context(
|
|
218
239
|
connection_overrides: dict | None = None,
|
snowflake/cli/api/config.py
CHANGED
|
@@ -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[
|
|
290
|
-
value = get_config_value(*path, key=key, 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:
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -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.
|
|
101
|
+
self.merge_with_config(**connection_config)
|
|
91
102
|
return self
|
|
92
103
|
|
|
93
104
|
def __repr__(self) -> str:
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -62,6 +62,10 @@ class ObjectType(Enum):
|
|
|
62
62
|
"image-repository", "image repository", "image repositories"
|
|
63
63
|
)
|
|
64
64
|
GIT_REPOSITORY = ObjectNames("git-repository", "git repository", "git repositories")
|
|
65
|
+
APPLICATION = ObjectNames("application", "application", "applications")
|
|
66
|
+
APPLICATION_PACKAGE = ObjectNames(
|
|
67
|
+
"application-package", "application package", "application packages"
|
|
68
|
+
)
|
|
65
69
|
|
|
66
70
|
def __str__(self):
|
|
67
71
|
"""This makes using this Enum easier in formatted string"""
|
|
@@ -69,7 +73,11 @@ class ObjectType(Enum):
|
|
|
69
73
|
|
|
70
74
|
|
|
71
75
|
OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType}
|
|
72
|
-
|
|
76
|
+
UNSUPPORTED_OBJECTS = {
|
|
77
|
+
ObjectType.APPLICATION.value.cli_name,
|
|
78
|
+
ObjectType.APPLICATION_PACKAGE.value.cli_name,
|
|
79
|
+
}
|
|
80
|
+
SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS)
|
|
73
81
|
|
|
74
82
|
# Scope names here must replace spaces with '-'. For example 'compute pool' is 'compute-pool'.
|
|
75
83
|
VALID_SCOPES = ["database", "schema", "compute-pool"]
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
2
4
|
from typing import Generic, Type, TypeVar, get_args
|
|
3
5
|
|
|
4
6
|
from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
|
|
7
|
+
from snowflake.cli.api.cli_global_context import span
|
|
8
|
+
from snowflake.cli.api.identifiers import FQN
|
|
5
9
|
from snowflake.cli.api.sql_execution import SqlExecutor
|
|
10
|
+
from snowflake.connector import SnowflakeConnection
|
|
11
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class EntityActions(str, Enum):
|
|
@@ -16,10 +22,47 @@ class EntityActions(str, Enum):
|
|
|
16
22
|
VERSION_CREATE = "action_version_create"
|
|
17
23
|
VERSION_DROP = "action_version_drop"
|
|
18
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
|
+
|
|
19
37
|
|
|
20
38
|
T = TypeVar("T")
|
|
21
39
|
|
|
22
40
|
|
|
41
|
+
def attach_spans_to_entity_actions(entity_name: str):
|
|
42
|
+
"""
|
|
43
|
+
Class decorator for EntityBase subclasses to automatically wrap
|
|
44
|
+
every implemented entity action method with a metrics span
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
entity_name (str): Custom name for entity type to be displayed in metrics
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def decorator(cls: type[T]) -> type[T]:
|
|
51
|
+
for attr_name, attr_value in vars(cls).items():
|
|
52
|
+
is_entity_action = attr_name in [
|
|
53
|
+
enum_member for enum_member in EntityActions
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
if is_entity_action and callable(attr_value):
|
|
57
|
+
attr_name_without_action_prefix = attr_name.partition("_")[2]
|
|
58
|
+
span_name = f"action.{entity_name}.{attr_name_without_action_prefix}"
|
|
59
|
+
action_with_span = span(span_name)(attr_value)
|
|
60
|
+
setattr(cls, attr_name, action_with_span)
|
|
61
|
+
return cls
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
65
|
+
|
|
23
66
|
class EntityBase(Generic[T]):
|
|
24
67
|
"""
|
|
25
68
|
Base class for the fully-featured entity classes.
|
|
@@ -29,6 +72,10 @@ class EntityBase(Generic[T]):
|
|
|
29
72
|
self._entity_model = entity_model
|
|
30
73
|
self._workspace_ctx = workspace_ctx
|
|
31
74
|
|
|
75
|
+
@property
|
|
76
|
+
def entity_id(self) -> str:
|
|
77
|
+
return self._entity_model.entity_id # type: ignore
|
|
78
|
+
|
|
32
79
|
@classmethod
|
|
33
80
|
def get_entity_model_type(cls) -> Type[T]:
|
|
34
81
|
"""
|
|
@@ -52,6 +99,44 @@ class EntityBase(Generic[T]):
|
|
|
52
99
|
"""
|
|
53
100
|
return getattr(self, action)(action_ctx, *args, **kwargs)
|
|
54
101
|
|
|
102
|
+
@property
|
|
103
|
+
def root(self) -> Path:
|
|
104
|
+
return self._workspace_ctx.project_root
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def identifier(self) -> str:
|
|
108
|
+
return self.model.fqn.sql_identifier
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def fqn(self) -> FQN:
|
|
112
|
+
return self._entity_model.fqn # type: ignore[attr-defined]
|
|
113
|
+
|
|
114
|
+
@functools.cached_property
|
|
115
|
+
def _sql_executor(
|
|
116
|
+
self,
|
|
117
|
+
) -> SqlExecutor:
|
|
118
|
+
return get_sql_executor()
|
|
119
|
+
|
|
120
|
+
def _execute_query(self, sql: str) -> SnowflakeCursor:
|
|
121
|
+
return self._sql_executor.execute_query(sql)
|
|
122
|
+
|
|
123
|
+
@functools.cached_property
|
|
124
|
+
def _conn(self) -> SnowflakeConnection:
|
|
125
|
+
return self._sql_executor._conn # noqa
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def model(self):
|
|
129
|
+
return self._entity_model
|
|
130
|
+
|
|
131
|
+
def get_usage_grant_sql(self, app_role: str) -> str:
|
|
132
|
+
return f"GRANT USAGE ON {self.model.type.upper()} {self.identifier} TO ROLE {app_role};"
|
|
133
|
+
|
|
134
|
+
def get_describe_sql(self) -> str:
|
|
135
|
+
return f"DESCRIBE {self.model.type.upper()} {self.identifier};"
|
|
136
|
+
|
|
137
|
+
def get_drop_sql(self) -> str:
|
|
138
|
+
return f"DROP {self.model.type.upper()} {self.identifier};"
|
|
139
|
+
|
|
55
140
|
|
|
56
141
|
def get_sql_executor() -> SqlExecutor:
|
|
57
142
|
"""Returns an SQL Executor that uses the connection from the current CLI context"""
|
|
@@ -23,7 +23,7 @@ from snowflake.cli._plugins.stage.diff import (
|
|
|
23
23
|
to_stage_path,
|
|
24
24
|
)
|
|
25
25
|
from snowflake.cli._plugins.stage.utils import print_diff_to_console
|
|
26
|
-
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
26
|
+
from snowflake.cli.api.cli_global_context import get_cli_context, span
|
|
27
27
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
28
28
|
from snowflake.cli.api.entities.common import get_sql_executor
|
|
29
29
|
from snowflake.cli.api.errno import (
|
|
@@ -42,7 +42,6 @@ from snowflake.cli.api.rendering.sql_templates import (
|
|
|
42
42
|
)
|
|
43
43
|
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
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,6 +75,7 @@ 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,
|
|
@@ -212,7 +212,10 @@ def execute_post_deploy_hooks(
|
|
|
212
212
|
|
|
213
213
|
get_cli_context().metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 1)
|
|
214
214
|
|
|
215
|
-
with
|
|
215
|
+
with (
|
|
216
|
+
console.phase(f"Executing {deployed_object_type} post-deploy actions"),
|
|
217
|
+
get_cli_context().metrics.span("post_deploy_hooks"),
|
|
218
|
+
):
|
|
216
219
|
sql_scripts_paths = []
|
|
217
220
|
display_paths = []
|
|
218
221
|
for hook in post_deploy_hooks:
|
|
@@ -321,17 +324,15 @@ def drop_generic_object(
|
|
|
321
324
|
console.message(f"Dropped {object_type} {object_name} successfully.")
|
|
322
325
|
|
|
323
326
|
|
|
324
|
-
def print_messages(
|
|
325
|
-
console: AbstractConsole, create_or_upgrade_cursor: Optional[SnowflakeCursor]
|
|
326
|
-
):
|
|
327
|
+
def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]):
|
|
327
328
|
"""
|
|
328
329
|
Shows messages in the console returned by the CREATE or UPGRADE
|
|
329
330
|
APPLICATION command.
|
|
330
331
|
"""
|
|
331
|
-
if not
|
|
332
|
+
if not cursor_results:
|
|
332
333
|
return
|
|
333
334
|
|
|
334
|
-
messages = [row[0] for row in
|
|
335
|
+
messages = [row[0] for row in cursor_results]
|
|
335
336
|
for message in messages:
|
|
336
337
|
console.warning(message)
|
|
337
338
|
console.message("")
|
snowflake/cli/api/errno.py
CHANGED
|
@@ -14,17 +14,74 @@
|
|
|
14
14
|
|
|
15
15
|
# General errors
|
|
16
16
|
NO_WAREHOUSE_SELECTED_IN_SESSION = 606
|
|
17
|
+
EMPTY_SQL_STATEMENT = 900
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
SQL_COMPILATION_ERROR = 1003
|
|
20
|
+
OBJECT_ALREADY_EXISTS_IN_DOMAIN = 1998
|
|
21
|
+
OBJECT_ALREADY_EXISTS = 2002
|
|
22
|
+
DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 # BASE_TABLE_OR_VIEW_NOT_FOUND
|
|
23
|
+
DUPLICATE_COLUMN_NAME = 2025
|
|
24
|
+
VIEW_EXPANSION_FAILED = 2037
|
|
25
|
+
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = (
|
|
26
|
+
2043 # OBJECT_DOES_NOT_EXIST_OR_CANNOT_PERFORM_OPERATION
|
|
27
|
+
)
|
|
28
|
+
INSUFFICIENT_PRIVILEGES = 3001 # NOT_AUTHORIZED
|
|
29
|
+
INVALID_OBJECT_TYPE_FOR_SPECIFIED_PRIVILEGE = 3008
|
|
30
|
+
ROLE_NOT_ASSIGNED = 3013
|
|
31
|
+
NO_INDIVIDUAL_PRIVS = 3028
|
|
32
|
+
OBJECT_ALREADY_EXISTS_NO_PRIVILEGES = 3041
|
|
21
33
|
|
|
22
34
|
# Native Apps
|
|
35
|
+
APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND = 93003
|
|
36
|
+
APPLICATION_FILE_NOT_FOUND_ON_STAGE = 93009
|
|
37
|
+
CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE = 93011
|
|
38
|
+
CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE = 93012
|
|
39
|
+
APPLICATION_PACKAGE_VERSION_ALREADY_EXISTS = 93030
|
|
40
|
+
APPLICATION_PACKAGE_VERSION_NAME_TOO_LONG = 93035
|
|
41
|
+
APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST = 93036
|
|
42
|
+
APPLICATION_PACKAGE_MAX_VERSIONS_HIT = 93037
|
|
23
43
|
CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION = 93044
|
|
24
44
|
CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES = 93045
|
|
25
45
|
ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046
|
|
46
|
+
NO_VERSIONS_AVAILABLE_FOR_ACCOUNT = 93054
|
|
26
47
|
NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055
|
|
27
48
|
APPLICATION_NO_LONGER_AVAILABLE = 93079
|
|
49
|
+
APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082
|
|
50
|
+
APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE = 93083
|
|
51
|
+
APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT = 93084
|
|
52
|
+
VERSION_REFERENCED_BY_RELEASE_DIRECTIVE = 93088
|
|
53
|
+
APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148
|
|
54
|
+
CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118
|
|
28
55
|
APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
|
|
56
|
+
APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168
|
|
57
|
+
APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197
|
|
58
|
+
NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301
|
|
59
|
+
NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302
|
|
60
|
+
NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR = 93303
|
|
61
|
+
NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX = 93300
|
|
29
62
|
APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321
|
|
30
63
|
CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
|
|
64
|
+
VERSION_NOT_ADDED_TO_RELEASE_CHANNEL = 512008
|
|
65
|
+
CANNOT_DISABLE_RELEASE_CHANNELS = 512001
|
|
66
|
+
RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND = 93036
|
|
67
|
+
RELEASE_DIRECTIVE_DOES_NOT_EXIST = 93090
|
|
68
|
+
VERSION_DOES_NOT_EXIST = 93031
|
|
69
|
+
VERSION_NOT_IN_RELEASE_CHANNEL = 512010
|
|
70
|
+
ACCOUNT_DOES_NOT_EXIST = 1999
|
|
71
|
+
ACCOUNT_HAS_TOO_MANY_QUALIFIERS = 906
|
|
72
|
+
CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS = 512017
|
|
73
|
+
VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL = 512005
|
|
74
|
+
MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED = 512004
|
|
75
|
+
MAX_UNBOUND_VERSIONS_REACHED = 512023
|
|
76
|
+
CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL = 512021
|
|
77
|
+
TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE = 93091
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
ERR_JAVASCRIPT_EXECUTION = 100132
|
|
81
|
+
|
|
82
|
+
SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL = 397007
|
|
83
|
+
SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND = 397012
|
|
84
|
+
SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW = 397013
|
|
85
|
+
|
|
86
|
+
NO_REFERENCE_SET_FOR_DEFINITION = 505019
|
|
87
|
+
NO_ACTIVE_REF_DEFINITION_WITH_REF_NAME_IN_APPLICATION = 505026
|
|
@@ -24,20 +24,31 @@ from snowflake.cli.api.config import (
|
|
|
24
24
|
|
|
25
25
|
class BooleanFlag(NamedTuple):
|
|
26
26
|
name: str
|
|
27
|
-
default: bool = False
|
|
27
|
+
default: bool | None = False
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@unique
|
|
31
31
|
class FeatureFlagMixin(Enum):
|
|
32
|
-
def
|
|
32
|
+
def get_value(self) -> bool | None:
|
|
33
33
|
return get_config_bool_value(
|
|
34
34
|
*FEATURE_FLAGS_SECTION_PATH,
|
|
35
35
|
key=self.value.name.lower(),
|
|
36
36
|
default=self.value.default,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
return
|
|
39
|
+
def is_enabled(self) -> bool:
|
|
40
|
+
return self.get_value() is True
|
|
41
|
+
|
|
42
|
+
def is_disabled(self) -> bool:
|
|
43
|
+
return self.get_value() is False
|
|
44
|
+
|
|
45
|
+
def is_set(self) -> bool:
|
|
46
|
+
return (
|
|
47
|
+
get_config_bool_value(
|
|
48
|
+
*FEATURE_FLAGS_SECTION_PATH, key=self.value.name.lower(), default=None
|
|
49
|
+
)
|
|
50
|
+
is not None
|
|
51
|
+
)
|
|
41
52
|
|
|
42
53
|
def env_variable(self):
|
|
43
54
|
return get_env_variable_name(*FEATURE_FLAGS_SECTION_PATH, key=self.value.name)
|
|
@@ -52,3 +63,8 @@ class FeatureFlag(FeatureFlagMixin):
|
|
|
52
63
|
ENABLE_STREAMLIT_VERSIONED_STAGE = BooleanFlag(
|
|
53
64
|
"ENABLE_STREAMLIT_VERSIONED_STAGE", False
|
|
54
65
|
)
|
|
66
|
+
ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
|
|
67
|
+
"ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
|
|
68
|
+
)
|
|
69
|
+
ENABLE_SPCS_SERVICE_EVENTS = BooleanFlag("ENABLE_SPCS_SERVICE_EVENTS", False)
|
|
70
|
+
ENABLE_SPCS_SERVICE_METRICS = BooleanFlag("ENABLE_SPCS_SERVICE_METRICS", False)
|
snowflake/cli/api/metrics.py
CHANGED
|
@@ -116,10 +116,8 @@ class CLIMetricsSpan:
|
|
|
116
116
|
children: Set[CLIMetricsSpan] = field(init=False, default_factory=set)
|
|
117
117
|
|
|
118
118
|
# private state
|
|
119
|
-
# start time of the step from
|
|
120
|
-
|
|
121
|
-
init=False, default_factory=lambda: time.monotonic()
|
|
122
|
-
)
|
|
119
|
+
# start time of the step from a performance counter in order to calculate execution time
|
|
120
|
+
_start_time: float = field(init=False, default_factory=time.perf_counter)
|
|
123
121
|
|
|
124
122
|
def __hash__(self) -> int:
|
|
125
123
|
return hash(self.span_id)
|
|
@@ -154,7 +152,7 @@ class CLIMetricsSpan:
|
|
|
154
152
|
if error:
|
|
155
153
|
self.error = error
|
|
156
154
|
|
|
157
|
-
self.execution_time = time.
|
|
155
|
+
self.execution_time = time.perf_counter() - self._start_time
|
|
158
156
|
|
|
159
157
|
def to_dict(self) -> Dict:
|
|
160
158
|
"""
|
|
@@ -191,9 +189,10 @@ class CLIMetrics:
|
|
|
191
189
|
_in_progress_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
|
|
192
190
|
# list of finished steps for telemetry to process
|
|
193
191
|
_completed_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
# clock time of a performance counter when this class was initialized
|
|
193
|
+
# to approximate when the command first started executing
|
|
194
|
+
_start_time: float = field(
|
|
195
|
+
init=False, default_factory=time.perf_counter, compare=False
|
|
197
196
|
)
|
|
198
197
|
|
|
199
198
|
def clone(self) -> CLIMetrics:
|
|
@@ -223,7 +222,7 @@ class CLIMetrics:
|
|
|
223
222
|
return self._in_progress_spans[-1] if len(self._in_progress_spans) > 0 else None
|
|
224
223
|
|
|
225
224
|
@contextmanager
|
|
226
|
-
def
|
|
225
|
+
def span(self, name: str) -> Iterator[CLIMetricsSpan]:
|
|
227
226
|
"""
|
|
228
227
|
Starts a new span that tracks various metrics throughout its execution
|
|
229
228
|
|
|
@@ -236,7 +235,7 @@ class CLIMetrics:
|
|
|
236
235
|
"""
|
|
237
236
|
new_span = CLIMetricsSpan(
|
|
238
237
|
name=name,
|
|
239
|
-
start_time=time.
|
|
238
|
+
start_time=time.perf_counter() - self._start_time,
|
|
240
239
|
parent=self.current_span,
|
|
241
240
|
)
|
|
242
241
|
|
|
@@ -275,34 +274,29 @@ class CLIMetrics:
|
|
|
275
274
|
@property
|
|
276
275
|
def completed_spans(self) -> List[Dict]:
|
|
277
276
|
"""
|
|
278
|
-
Returns the completed spans tracked throughout a command
|
|
277
|
+
Returns the completed spans tracked throughout a command for reporting telemetry
|
|
279
278
|
|
|
280
279
|
Ensures that the spans we send are within the configured limits and marks
|
|
281
280
|
certain spans as trimmed if their children would bypass the limits we set
|
|
282
281
|
"""
|
|
283
282
|
# take spans breadth-first within the depth and total limits
|
|
284
283
|
# since we care more about the big picture than granularity
|
|
285
|
-
spans_to_report =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
key=lambda span: (span.span_depth, span.start_time),
|
|
294
|
-
)
|
|
284
|
+
spans_to_report = nsmallest(
|
|
285
|
+
n=self.SPAN_TOTAL_LIMIT,
|
|
286
|
+
iterable=(
|
|
287
|
+
span
|
|
288
|
+
for span in self._completed_spans
|
|
289
|
+
if span.span_depth <= self.SPAN_DEPTH_LIMIT
|
|
290
|
+
),
|
|
291
|
+
key=lambda span: (span.span_depth, span.start_time, span.execution_time),
|
|
295
292
|
)
|
|
296
293
|
|
|
297
|
-
|
|
298
|
-
sorted_spans_to_report = sorted(
|
|
299
|
-
spans_to_report, key=lambda span: span.start_time
|
|
300
|
-
)
|
|
294
|
+
spans_to_report_set = set(spans_to_report)
|
|
301
295
|
|
|
302
296
|
return [
|
|
303
297
|
{
|
|
304
298
|
**span.to_dict(),
|
|
305
|
-
CLIMetricsSpan.TRIMMED_KEY: not span.children <=
|
|
299
|
+
CLIMetricsSpan.TRIMMED_KEY: not span.children <= spans_to_report_set,
|
|
306
300
|
}
|
|
307
|
-
for span in
|
|
301
|
+
for span in spans_to_report
|
|
308
302
|
]
|
|
@@ -13,7 +13,6 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
|
|
|
13
13
|
bundle_artifacts,
|
|
14
14
|
)
|
|
15
15
|
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
|
|
16
|
-
from snowflake.cli._plugins.nativeapp.codegen.compiler import TEMPLATES_PROCESSOR
|
|
17
16
|
from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import (
|
|
18
17
|
TemplatesProcessor,
|
|
19
18
|
)
|
|
@@ -457,7 +456,7 @@ def _convert_templates_in_files(
|
|
|
457
456
|
artifact
|
|
458
457
|
for artifact in pkg_model.artifacts
|
|
459
458
|
for processor in artifact.processors
|
|
460
|
-
if processor.name ==
|
|
459
|
+
if processor.name.lower() == TemplatesProcessor.NAME
|
|
461
460
|
]
|
|
462
461
|
if not in_memory and artifacts_to_template:
|
|
463
462
|
metrics.set_counter(CLICounterField.TEMPLATES_PROCESSOR, 1)
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from
|
|
18
|
+
from types import UnionType
|
|
19
|
+
from typing import Any, Dict, List, Optional, Union, get_args, get_origin
|
|
19
20
|
|
|
20
21
|
from packaging.version import Version
|
|
21
22
|
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
22
23
|
from pydantic_core.core_schema import ValidationInfo
|
|
23
24
|
from snowflake.cli._plugins.nativeapp.entities.application import ApplicationEntityModel
|
|
25
|
+
from snowflake.cli._plugins.nativeapp.entities.application_package import (
|
|
26
|
+
ApplicationPackageChildrenTypes,
|
|
27
|
+
ApplicationPackageEntityModel,
|
|
28
|
+
)
|
|
24
29
|
from snowflake.cli.api.project.errors import SchemaValidationError
|
|
25
30
|
from snowflake.cli.api.project.schemas.entities.common import (
|
|
26
31
|
TargetField,
|
|
@@ -159,6 +164,12 @@ class DefinitionV20(_ProjectDefinitionBase):
|
|
|
159
164
|
target_object = entity.from_
|
|
160
165
|
target_type = target_object.get_type()
|
|
161
166
|
cls._validate_target_field(target_key, target_type, entities)
|
|
167
|
+
elif entity.type == ApplicationPackageEntityModel.get_type():
|
|
168
|
+
for child_entity in entity.children:
|
|
169
|
+
target_key = child_entity.target
|
|
170
|
+
cls._validate_target_field(
|
|
171
|
+
target_key, ApplicationPackageChildrenTypes, entities
|
|
172
|
+
)
|
|
162
173
|
|
|
163
174
|
@classmethod
|
|
164
175
|
def _validate_target_field(
|
|
@@ -168,11 +179,20 @@ class DefinitionV20(_ProjectDefinitionBase):
|
|
|
168
179
|
raise ValueError(f"No such target: {target_key}")
|
|
169
180
|
|
|
170
181
|
# Validate the target type
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
if target_type:
|
|
183
|
+
actual_target_type = entities[target_key].__class__
|
|
184
|
+
if get_origin(target_type) in (Union, UnionType):
|
|
185
|
+
if actual_target_type not in get_args(target_type):
|
|
186
|
+
expected_types_str = ", ".join(
|
|
187
|
+
[t.__name__ for t in get_args(target_type)]
|
|
188
|
+
)
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Target type mismatch. Expected one of [{expected_types_str}], got {actual_target_type.__name__}"
|
|
191
|
+
)
|
|
192
|
+
elif target_type is not actual_target_type:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
|
|
195
|
+
)
|
|
176
196
|
|
|
177
197
|
@model_validator(mode="before")
|
|
178
198
|
@classmethod
|
|
@@ -200,6 +220,7 @@ class DefinitionV20(_ProjectDefinitionBase):
|
|
|
200
220
|
mixin_defs=data["mixins"],
|
|
201
221
|
)
|
|
202
222
|
entities[entity_name] = merged_values
|
|
223
|
+
|
|
203
224
|
return data
|
|
204
225
|
|
|
205
226
|
@classmethod
|
|
@@ -27,7 +27,7 @@ class Streamlit(UpdatableModel, ObjectIdentifierModel(object_name="Streamlit")):
|
|
|
27
27
|
title="Stage in which the app’s artifacts will be stored", default="streamlit"
|
|
28
28
|
)
|
|
29
29
|
query_warehouse: str = Field(
|
|
30
|
-
title="Snowflake warehouse to host the app", default=
|
|
30
|
+
title="Snowflake warehouse to host the app", default=None
|
|
31
31
|
)
|
|
32
32
|
main_file: Optional[Path] = Field(
|
|
33
33
|
title="Entrypoint file of the Streamlit app", default="streamlit_app.py"
|
|
@@ -148,6 +148,10 @@ def unquote_identifier(identifier: str) -> str:
|
|
|
148
148
|
string for a LIKE clause, or to match an identifier passed back as
|
|
149
149
|
a value from a SQL statement.
|
|
150
150
|
"""
|
|
151
|
+
# ensure input is a valid identifier - otherwise, it could accidentally uppercase
|
|
152
|
+
# a quoted identifier
|
|
153
|
+
identifier = to_identifier(identifier)
|
|
154
|
+
|
|
151
155
|
if match := re.fullmatch(QUOTED_IDENTIFIER_REGEX, identifier):
|
|
152
156
|
return match.group(1).replace('""', '"')
|
|
153
157
|
# unquoted identifiers are internally represented as uppercase
|
|
@@ -276,3 +280,44 @@ def append_test_resource_suffix(identifier: str) -> str:
|
|
|
276
280
|
# Otherwise just append the string, don't add quotes
|
|
277
281
|
# in case the user doesn't want them
|
|
278
282
|
return f"{identifier}{suffix}"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def same_identifiers(id1: str, id2: str) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Returns whether two identifiers refer to the same object.
|
|
288
|
+
|
|
289
|
+
Two unquoted identifiers are considered the same if they are equal when both are converted to uppercase
|
|
290
|
+
Two quoted identifiers are considered the same if they are exactly equal
|
|
291
|
+
An unquoted identifier and a quoted identifier are considered the same
|
|
292
|
+
if the quoted identifier is equal to the uppercase version of the unquoted identifier
|
|
293
|
+
"""
|
|
294
|
+
# Canonicalize the identifiers by converting unquoted identifiers to uppercase and leaving quoted identifiers as is
|
|
295
|
+
canonical_id1 = id1.upper() if is_valid_unquoted_identifier(id1) else id1
|
|
296
|
+
canonical_id2 = id2.upper() if is_valid_unquoted_identifier(id2) else id2
|
|
297
|
+
|
|
298
|
+
# The canonical identifiers are equal if they are equal when both are quoted
|
|
299
|
+
# (if they are already quoted, this is a no-op)
|
|
300
|
+
return to_quoted_identifier(canonical_id1) == to_quoted_identifier(canonical_id2)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def sql_match(*, pattern: str, value: str) -> bool:
|
|
304
|
+
"""
|
|
305
|
+
Returns whether the value matches the pattern when used with LIKE in Snowflake.
|
|
306
|
+
Compares the 2 input and ignores the case.
|
|
307
|
+
"""
|
|
308
|
+
value = unquote_identifier(value)
|
|
309
|
+
return (
|
|
310
|
+
re.fullmatch(
|
|
311
|
+
pattern.replace(r"%", ".*").replace(r"_", "."), value, re.IGNORECASE
|
|
312
|
+
)
|
|
313
|
+
is not None
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def identifier_in_list(identifier: str, identifier_list: list[str]) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Returns whether the identifier is in the list of identifiers.
|
|
320
|
+
"""
|
|
321
|
+
return any(
|
|
322
|
+
same_identifiers(identifier, id_from_list) for id_from_list in identifier_list
|
|
323
|
+
)
|
snowflake/cli/api/rest_api.py
CHANGED
|
@@ -155,8 +155,9 @@ class RestApi:
|
|
|
155
155
|
raise SchemaNotDefinedException(
|
|
156
156
|
"Schema not defined in connection. Please try again with `--schema` flag."
|
|
157
157
|
)
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
# temporarily disable this check due to an issue on server side: SNOW-1747450
|
|
159
|
+
# if not self._schema_exists(db_name=db, schema_name=schema):
|
|
160
|
+
# raise SchemaNotExistsException(f"Schema '{schema}' does not exist.")
|
|
160
161
|
if self.get_endpoint_exists(
|
|
161
162
|
url := f"{SF_REST_API_URL_PREFIX}/databases/{self.conn.database}/schemas/{self.conn.schema}/{plural_object_type}/"
|
|
162
163
|
):
|