snowflake-cli 3.10.0__py3-none-any.whl → 3.11.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/auth/__init__.py +13 -0
- snowflake/cli/_app/auth/errors.py +28 -0
- snowflake/cli/_app/auth/oidc_providers.py +393 -0
- snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -2
- snowflake/cli/_app/constants.py +10 -0
- snowflake/cli/_app/snow_connector.py +35 -0
- snowflake/cli/_plugins/auth/__init__.py +4 -2
- snowflake/cli/_plugins/auth/keypair/commands.py +2 -0
- snowflake/cli/_plugins/auth/oidc/__init__.py +13 -0
- snowflake/cli/_plugins/auth/oidc/commands.py +47 -0
- snowflake/cli/_plugins/auth/oidc/manager.py +66 -0
- snowflake/cli/_plugins/auth/oidc/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +37 -3
- snowflake/cli/_plugins/dbt/manager.py +7 -7
- snowflake/cli/_plugins/{project → dcm}/commands.py +113 -122
- snowflake/cli/_plugins/{project/project_entity_model.py → dcm/dcm_project_entity_model.py} +5 -5
- snowflake/cli/_plugins/dcm/manager.py +96 -0
- snowflake/cli/_plugins/{project → dcm}/plugin_spec.py +1 -1
- snowflake/cli/_plugins/notebook/notebook_entity.py +2 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +8 -1
- snowflake/cli/_plugins/object/command_aliases.py +16 -1
- snowflake/cli/_plugins/object/commands.py +27 -1
- snowflake/cli/_plugins/object/manager.py +12 -1
- snowflake/cli/_plugins/snowpark/commands.py +8 -1
- snowflake/cli/api/commands/decorators.py +7 -0
- snowflake/cli/api/commands/flags.py +26 -0
- snowflake/cli/api/config.py +24 -0
- snowflake/cli/api/connections.py +1 -0
- snowflake/cli/api/constants.py +2 -2
- snowflake/cli/api/project/schemas/entities/entities.py +6 -6
- snowflake/cli/api/rest_api.py +1 -0
- snowflake/cli/api/stage_path.py +4 -0
- snowflake/cli/api/utils/dict_utils.py +42 -1
- {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/METADATA +13 -39
- {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/RECORD +40 -33
- snowflake/cli/_plugins/project/manager.py +0 -134
- /snowflake/cli/_plugins/{project → dcm}/__init__.py +0 -0
- {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import List
|
|
16
|
+
|
|
17
|
+
from snowflake.cli._plugins.dcm.dcm_project_entity_model import DCMProjectEntityModel
|
|
18
|
+
from snowflake.cli._plugins.stage.manager import StageManager
|
|
19
|
+
from snowflake.cli.api.commands.utils import parse_key_value_variables
|
|
20
|
+
from snowflake.cli.api.identifiers import FQN
|
|
21
|
+
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
22
|
+
from snowflake.cli.api.stage_path import StagePath
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DCMProjectManager(SqlExecutionMixin):
|
|
26
|
+
def execute(
|
|
27
|
+
self,
|
|
28
|
+
project_name: FQN,
|
|
29
|
+
from_stage: str,
|
|
30
|
+
configuration: str | None = None,
|
|
31
|
+
variables: List[str] | None = None,
|
|
32
|
+
dry_run: bool = False,
|
|
33
|
+
alias: str | None = None,
|
|
34
|
+
output_path: str | None = None,
|
|
35
|
+
):
|
|
36
|
+
|
|
37
|
+
query = f"EXECUTE DCM PROJECT {project_name.sql_identifier}"
|
|
38
|
+
if dry_run:
|
|
39
|
+
query += " PLAN"
|
|
40
|
+
else:
|
|
41
|
+
query += " DEPLOY"
|
|
42
|
+
if alias:
|
|
43
|
+
query += f" AS {alias}"
|
|
44
|
+
if configuration or variables:
|
|
45
|
+
query += f" USING"
|
|
46
|
+
if configuration:
|
|
47
|
+
query += f" CONFIGURATION {configuration}"
|
|
48
|
+
if variables:
|
|
49
|
+
query += StageManager.parse_execute_variables(
|
|
50
|
+
parse_key_value_variables(variables)
|
|
51
|
+
).removeprefix(" using")
|
|
52
|
+
stage_path = StagePath.from_stage_str(from_stage)
|
|
53
|
+
query += f" FROM {stage_path.absolute_path()}"
|
|
54
|
+
if output_path:
|
|
55
|
+
output_stage_path = StagePath.from_stage_str(output_path)
|
|
56
|
+
query += f" OUTPUT_PATH {output_stage_path.absolute_path()}"
|
|
57
|
+
return self.execute_query(query=query)
|
|
58
|
+
|
|
59
|
+
def create(self, project: DCMProjectEntityModel) -> None:
|
|
60
|
+
query = f"CREATE DCM PROJECT {project.fqn.sql_identifier}"
|
|
61
|
+
self.execute_query(query)
|
|
62
|
+
|
|
63
|
+
def _create_version(
|
|
64
|
+
self,
|
|
65
|
+
project_name: FQN,
|
|
66
|
+
from_stage: str,
|
|
67
|
+
alias: str | None = None,
|
|
68
|
+
comment: str | None = None,
|
|
69
|
+
):
|
|
70
|
+
stage_path = StagePath.from_stage_str(from_stage)
|
|
71
|
+
query = f"ALTER DCM PROJECT {project_name.identifier} ADD VERSION"
|
|
72
|
+
if alias:
|
|
73
|
+
query += f" IF NOT EXISTS {alias}"
|
|
74
|
+
query += f" FROM {stage_path.absolute_path(at_prefix=True)}"
|
|
75
|
+
if comment:
|
|
76
|
+
query += f" COMMENT = '{comment}'"
|
|
77
|
+
return self.execute_query(query=query)
|
|
78
|
+
|
|
79
|
+
def list_versions(self, project_name: FQN):
|
|
80
|
+
query = f"SHOW VERSIONS IN DCM PROJECT {project_name.identifier}"
|
|
81
|
+
return self.execute_query(query=query)
|
|
82
|
+
|
|
83
|
+
def drop_deployment(
|
|
84
|
+
self,
|
|
85
|
+
project_name: FQN,
|
|
86
|
+
version_name: str,
|
|
87
|
+
if_exists: bool = False,
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Drops a version from the DCM Project.
|
|
91
|
+
"""
|
|
92
|
+
query = f"ALTER DCM PROJECT {project_name.identifier} DROP VERSION"
|
|
93
|
+
if if_exists:
|
|
94
|
+
query += " IF EXISTS"
|
|
95
|
+
query += f" {version_name}"
|
|
96
|
+
return self.execute_query(query=query)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from snowflake.cli._plugins.
|
|
15
|
+
from snowflake.cli._plugins.dcm import commands
|
|
16
16
|
from snowflake.cli.api.plugins.command import (
|
|
17
17
|
SNOWCLI_ROOT_COMMAND_PATH,
|
|
18
18
|
CommandSpec,
|
|
@@ -60,6 +60,8 @@ class NotebookEntity(EntityBase[NotebookEntityModel]):
|
|
|
60
60
|
query += f"\nCOMPUTE_POOL = '{self.model.compute_pool}'"
|
|
61
61
|
if self.model.runtime_name:
|
|
62
62
|
query += f"\nRUNTIME_NAME = '{self.model.runtime_name}'"
|
|
63
|
+
if self.model.runtime_environment_version and not self.model.compute_pool:
|
|
64
|
+
query += f"\nRUNTIME_ENVIRONMENT_VERSION = '{self.model.runtime_environment_version}'"
|
|
63
65
|
|
|
64
66
|
query += (
|
|
65
67
|
";\n// Cannot use IDENTIFIER(...)"
|
|
@@ -20,6 +20,9 @@ class NotebookEntityModel(EntityModelBaseWithArtifacts):
|
|
|
20
20
|
)
|
|
21
21
|
notebook_file: Path = Field(title="Notebook file")
|
|
22
22
|
query_warehouse: str = Field(title="Snowflake warehouse to execute the notebook")
|
|
23
|
+
runtime_environment_version: Optional[str] = Field(
|
|
24
|
+
title="Runtime environment version", default=None
|
|
25
|
+
)
|
|
23
26
|
compute_pool: Optional[str] = Field(
|
|
24
27
|
title="Compute pool to run the notebook in", default=None
|
|
25
28
|
)
|
|
@@ -37,6 +40,10 @@ class NotebookEntityModel(EntityModelBaseWithArtifacts):
|
|
|
37
40
|
def validate_container_setup(self):
|
|
38
41
|
if self.compute_pool and not self.runtime_name:
|
|
39
42
|
raise ValueError("compute_pool is specified without runtime_name")
|
|
40
|
-
if self.runtime_name and not self.compute_pool
|
|
43
|
+
if self.runtime_name and not self.compute_pool:
|
|
41
44
|
raise ValueError("runtime_name is specified without compute_pool")
|
|
45
|
+
if self.compute_pool and self.runtime_environment_version:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"runtime_environment_version is only applicable for notebooks using warehouse, not compute pool"
|
|
48
|
+
)
|
|
42
49
|
return self
|
|
@@ -22,8 +22,10 @@ from snowflake.cli._plugins.object.commands import (
|
|
|
22
22
|
ScopeOption,
|
|
23
23
|
describe,
|
|
24
24
|
drop,
|
|
25
|
+
limit_option_,
|
|
25
26
|
list_,
|
|
26
27
|
scope_option, # noqa: F401
|
|
28
|
+
terse_option_,
|
|
27
29
|
)
|
|
28
30
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
29
31
|
from snowflake.cli.api.constants import ObjectType
|
|
@@ -37,6 +39,8 @@ def add_object_command_aliases(
|
|
|
37
39
|
like_option: Optional[typer.Option],
|
|
38
40
|
scope_option: Optional[typer.Option],
|
|
39
41
|
ommit_commands: Optional[List[str]] = None,
|
|
42
|
+
terse_option: Optional[typer.Option] = None,
|
|
43
|
+
limit_option: Optional[typer.Option] = None,
|
|
40
44
|
):
|
|
41
45
|
if ommit_commands is None:
|
|
42
46
|
ommit_commands = list()
|
|
@@ -47,11 +51,18 @@ def add_object_command_aliases(
|
|
|
47
51
|
if not scope_option:
|
|
48
52
|
|
|
49
53
|
@app.command("list", requires_connection=True)
|
|
50
|
-
def list_cmd(
|
|
54
|
+
def list_cmd(
|
|
55
|
+
like: str = like_option, # type: ignore
|
|
56
|
+
terse: bool = terse_option if terse_option else terse_option_(), # type: ignore
|
|
57
|
+
limit: Optional[int] = limit_option if limit_option else limit_option_(), # type: ignore
|
|
58
|
+
**options,
|
|
59
|
+
):
|
|
51
60
|
return list_(
|
|
52
61
|
object_type=object_type.value.cli_name,
|
|
53
62
|
like=like,
|
|
54
63
|
scope=ScopeOption.default,
|
|
64
|
+
terse=terse,
|
|
65
|
+
limit=limit,
|
|
55
66
|
**options,
|
|
56
67
|
)
|
|
57
68
|
|
|
@@ -61,12 +72,16 @@ def add_object_command_aliases(
|
|
|
61
72
|
def list_cmd(
|
|
62
73
|
like: str = like_option, # type: ignore
|
|
63
74
|
scope: Tuple[str, str] = scope_option, # type: ignore
|
|
75
|
+
terse: bool = terse_option if terse_option else terse_option_(), # type: ignore
|
|
76
|
+
limit: Optional[int] = limit_option if limit_option else limit_option_(), # type: ignore
|
|
64
77
|
**options,
|
|
65
78
|
):
|
|
66
79
|
return list_(
|
|
67
80
|
object_type=object_type.value.cli_name,
|
|
68
81
|
like=like,
|
|
69
82
|
scope=scope,
|
|
83
|
+
terse=terse,
|
|
84
|
+
limit=limit,
|
|
70
85
|
**options,
|
|
71
86
|
)
|
|
72
87
|
|
|
@@ -94,6 +94,24 @@ def scope_option(help_example: str):
|
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def terse_option_():
|
|
98
|
+
return typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--terse",
|
|
101
|
+
help=f"Returns only a subset of available columns.",
|
|
102
|
+
hidden=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def limit_option_():
|
|
107
|
+
return typer.Option(
|
|
108
|
+
None,
|
|
109
|
+
"--limit",
|
|
110
|
+
help=f"Limits the maximum number of rows returned.",
|
|
111
|
+
hidden=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
97
115
|
ScopeOption = scope_option(
|
|
98
116
|
help_example="`list table --in database my_db`. Some object types have specialized scopes (e.g. list service --in compute-pool my_pool)"
|
|
99
117
|
)
|
|
@@ -110,11 +128,19 @@ def list_(
|
|
|
110
128
|
object_type: str = ObjectArgument,
|
|
111
129
|
like: str = LikeOption,
|
|
112
130
|
scope: Tuple[str, str] = ScopeOption,
|
|
131
|
+
terse: Optional[bool] = terse_option_(),
|
|
132
|
+
limit: Optional[int] = limit_option_(),
|
|
113
133
|
**options,
|
|
114
134
|
):
|
|
115
135
|
_scope_validate(object_type, scope)
|
|
116
136
|
return QueryResult(
|
|
117
|
-
ObjectManager().show(
|
|
137
|
+
ObjectManager().show(
|
|
138
|
+
object_type=object_type,
|
|
139
|
+
like=like,
|
|
140
|
+
scope=scope,
|
|
141
|
+
terse=terse,
|
|
142
|
+
limit=limit,
|
|
143
|
+
)
|
|
118
144
|
)
|
|
119
145
|
|
|
120
146
|
|
|
@@ -44,14 +44,25 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
44
44
|
object_type: str,
|
|
45
45
|
like: Optional[str] = None,
|
|
46
46
|
scope: Union[Tuple[str, str], Tuple[None, None]] = (None, None),
|
|
47
|
+
terse: Optional[bool] = False,
|
|
48
|
+
limit: Optional[int] = None,
|
|
47
49
|
**kwargs,
|
|
48
50
|
) -> SnowflakeCursor:
|
|
49
51
|
object_name = _get_object_names(object_type).sf_plural_name
|
|
50
|
-
|
|
52
|
+
query_parts = ["show"]
|
|
53
|
+
|
|
54
|
+
if terse:
|
|
55
|
+
query_parts.append("terse")
|
|
56
|
+
|
|
57
|
+
query_parts.append(object_name)
|
|
58
|
+
query = " ".join(query_parts)
|
|
59
|
+
|
|
51
60
|
if like:
|
|
52
61
|
query += f" like '{like}'"
|
|
53
62
|
if scope[0] is not None:
|
|
54
63
|
query += f" in {scope[0].replace('-', ' ')} {scope[1]}"
|
|
64
|
+
if limit is not None:
|
|
65
|
+
query += f" limit {limit}"
|
|
55
66
|
return self.execute_query(query, **kwargs)
|
|
56
67
|
|
|
57
68
|
def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor:
|
|
@@ -448,7 +448,14 @@ def list_(
|
|
|
448
448
|
**options,
|
|
449
449
|
):
|
|
450
450
|
"""Lists all available procedures or functions."""
|
|
451
|
-
return object_list(
|
|
451
|
+
return object_list(
|
|
452
|
+
object_type=object_type.value,
|
|
453
|
+
like=like,
|
|
454
|
+
scope=scope,
|
|
455
|
+
terse=None,
|
|
456
|
+
limit=None,
|
|
457
|
+
**options,
|
|
458
|
+
)
|
|
452
459
|
|
|
453
460
|
|
|
454
461
|
@app.command("drop", requires_connection=True)
|
|
@@ -57,6 +57,7 @@ from snowflake.cli.api.commands.flags import (
|
|
|
57
57
|
UserOption,
|
|
58
58
|
VerboseOption,
|
|
59
59
|
WarehouseOption,
|
|
60
|
+
WorkloadIdentityProviderOption,
|
|
60
61
|
experimental_option,
|
|
61
62
|
project_definition_option,
|
|
62
63
|
project_env_overrides_option,
|
|
@@ -262,6 +263,12 @@ GLOBAL_CONNECTION_OPTIONS = [
|
|
|
262
263
|
annotation=Optional[str],
|
|
263
264
|
default=AuthenticatorOption,
|
|
264
265
|
),
|
|
266
|
+
inspect.Parameter(
|
|
267
|
+
"workload_identity_provider",
|
|
268
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
269
|
+
annotation=Optional[str],
|
|
270
|
+
default=WorkloadIdentityProviderOption,
|
|
271
|
+
),
|
|
265
272
|
inspect.Parameter(
|
|
266
273
|
"private_key_file",
|
|
267
274
|
inspect.Parameter.KEYWORD_ONLY,
|
|
@@ -34,6 +34,7 @@ from snowflake.cli.api.identifiers import FQN
|
|
|
34
34
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
35
35
|
from snowflake.cli.api.secret import SecretType
|
|
36
36
|
from snowflake.cli.api.stage_path import StagePath
|
|
37
|
+
from snowflake.connector.auth.workload_identity import ApiFederatedAuthenticationType
|
|
37
38
|
|
|
38
39
|
DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
|
|
39
40
|
|
|
@@ -150,6 +151,21 @@ def _password_callback(value: str):
|
|
|
150
151
|
return _connection_callback("password")(value)
|
|
151
152
|
|
|
152
153
|
|
|
154
|
+
def _workload_identity_provider_callback(value: str):
|
|
155
|
+
if value is not None:
|
|
156
|
+
try:
|
|
157
|
+
# Validate that the value is one of the enum values
|
|
158
|
+
ApiFederatedAuthenticationType(value)
|
|
159
|
+
except ValueError:
|
|
160
|
+
valid_values = [e.value for e in ApiFederatedAuthenticationType]
|
|
161
|
+
raise ClickException(
|
|
162
|
+
f"Invalid workload identity provider '{value}'. "
|
|
163
|
+
f"Valid values are: {', '.join(valid_values)}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return _connection_callback("workload_identity_provider")(value)
|
|
167
|
+
|
|
168
|
+
|
|
153
169
|
PasswordOption = typer.Option(
|
|
154
170
|
None,
|
|
155
171
|
"--password",
|
|
@@ -170,6 +186,16 @@ AuthenticatorOption = typer.Option(
|
|
|
170
186
|
rich_help_panel=_CONNECTION_SECTION,
|
|
171
187
|
)
|
|
172
188
|
|
|
189
|
+
WorkloadIdentityProviderOption = typer.Option(
|
|
190
|
+
None,
|
|
191
|
+
"--workload-identity-provider",
|
|
192
|
+
help="Workload identity provider (AWS, AZURE, GCP, OIDC). Overrides the value specified for the connection",
|
|
193
|
+
hide_input=True,
|
|
194
|
+
callback=_workload_identity_provider_callback,
|
|
195
|
+
show_default=False,
|
|
196
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
197
|
+
)
|
|
198
|
+
|
|
173
199
|
PrivateKeyPathOption = typer.Option(
|
|
174
200
|
None,
|
|
175
201
|
"--private-key-file",
|
snowflake/cli/api/config.py
CHANGED
|
@@ -34,6 +34,7 @@ from snowflake.cli.api.secure_utils import (
|
|
|
34
34
|
file_permissions_are_strict,
|
|
35
35
|
windows_get_not_whitelisted_users_with_access,
|
|
36
36
|
)
|
|
37
|
+
from snowflake.cli.api.utils.dict_utils import remove_key_from_nested_dict_if_exists
|
|
37
38
|
from snowflake.cli.api.utils.types import try_cast_to_bool
|
|
38
39
|
from snowflake.connector.compat import IS_WINDOWS
|
|
39
40
|
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
@@ -82,6 +83,7 @@ class ConnectionConfig:
|
|
|
82
83
|
warehouse: Optional[str] = None
|
|
83
84
|
role: Optional[str] = None
|
|
84
85
|
authenticator: Optional[str] = None
|
|
86
|
+
workload_identity_provider: Optional[str] = None
|
|
85
87
|
private_key_file: Optional[str] = None
|
|
86
88
|
token_file_path: Optional[str] = None
|
|
87
89
|
oauth_client_id: Optional[str] = None
|
|
@@ -158,6 +160,19 @@ def add_connection_to_proper_file(name: str, connection_config: ConnectionConfig
|
|
|
158
160
|
return CONFIG_MANAGER.file_path
|
|
159
161
|
|
|
160
162
|
|
|
163
|
+
def remove_connection_from_proper_file(name: str):
|
|
164
|
+
if CONNECTIONS_FILE.exists():
|
|
165
|
+
existing_connections = _read_connections_toml()
|
|
166
|
+
if name not in existing_connections:
|
|
167
|
+
raise MissingConfigurationError(f"Connection {name} is not configured")
|
|
168
|
+
del existing_connections[name]
|
|
169
|
+
_update_connections_toml(existing_connections)
|
|
170
|
+
return CONNECTIONS_FILE
|
|
171
|
+
else:
|
|
172
|
+
unset_config_value(path=[CONNECTIONS_SECTION, name])
|
|
173
|
+
return CONFIG_MANAGER.file_path
|
|
174
|
+
|
|
175
|
+
|
|
161
176
|
_DEFAULT_LOGS_CONFIG = {
|
|
162
177
|
"save_logs": True,
|
|
163
178
|
"path": str(CONFIG_MANAGER.file_path.parent / "logs"),
|
|
@@ -228,6 +243,15 @@ def set_config_value(path: List[str], value: Any) -> None:
|
|
|
228
243
|
current_config_dict[path[-1]] = value
|
|
229
244
|
|
|
230
245
|
|
|
246
|
+
def unset_config_value(path: List[str]) -> None:
|
|
247
|
+
"""Unsets value in config.
|
|
248
|
+
For example to unset value for key "key" in section [a.b.c], call
|
|
249
|
+
unset_config_value(["a", "b", "c", "key"]).
|
|
250
|
+
"""
|
|
251
|
+
with _config_file() as conf_file_cache:
|
|
252
|
+
remove_key_from_nested_dict_if_exists(conf_file_cache, path)
|
|
253
|
+
|
|
254
|
+
|
|
231
255
|
def get_logs_config() -> dict:
|
|
232
256
|
logs_config = _DEFAULT_LOGS_CONFIG.copy()
|
|
233
257
|
if config_section_exists(*LOGS_SECTION_PATH):
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -45,6 +45,7 @@ class ConnectionContext:
|
|
|
45
45
|
user: Optional[str] = None
|
|
46
46
|
password: Optional[str] = field(default=None, repr=False)
|
|
47
47
|
authenticator: Optional[str] = None
|
|
48
|
+
workload_identity_provider: Optional[str] = None
|
|
48
49
|
private_key_file: Optional[str] = None
|
|
49
50
|
warehouse: Optional[str] = None
|
|
50
51
|
mfa_passcode: Optional[str] = None
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -36,6 +36,7 @@ class ObjectNames:
|
|
|
36
36
|
class ObjectType(Enum):
|
|
37
37
|
COMPUTE_POOL = ObjectNames("compute-pool", "compute pool", "compute pools")
|
|
38
38
|
DBT_PROJECT = ObjectNames("dbt-project", "dbt project", "dbt projects")
|
|
39
|
+
DCM_PROJECT = ObjectNames("dcm", "DCM Project", "DCM Projects")
|
|
39
40
|
DATABASE = ObjectNames("database", "database", "databases")
|
|
40
41
|
FUNCTION = ObjectNames("function", "function", "functions")
|
|
41
42
|
INTEGRATION = ObjectNames("integration", "integration", "integrations")
|
|
@@ -48,7 +49,6 @@ class ObjectType(Enum):
|
|
|
48
49
|
NETWORK_RULE = ObjectNames("network-rule", "network rule", "network rules")
|
|
49
50
|
NOTEBOOK = ObjectNames("notebook", "notebook", "notebooks")
|
|
50
51
|
PROCEDURE = ObjectNames("procedure", "procedure", "procedures")
|
|
51
|
-
PROJECT = ObjectNames("project", "project", "projects")
|
|
52
52
|
ROLE = ObjectNames("role", "role", "roles")
|
|
53
53
|
SCHEMA = ObjectNames("schema", "schema", "schemas")
|
|
54
54
|
SERVICE = ObjectNames("service", "service", "services")
|
|
@@ -79,7 +79,7 @@ OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType}
|
|
|
79
79
|
UNSUPPORTED_OBJECTS = {
|
|
80
80
|
ObjectType.APPLICATION.value.cli_name,
|
|
81
81
|
ObjectType.APPLICATION_PACKAGE.value.cli_name,
|
|
82
|
-
ObjectType.
|
|
82
|
+
ObjectType.DCM_PROJECT.value.cli_name,
|
|
83
83
|
ObjectType.DBT_PROJECT.value.cli_name,
|
|
84
84
|
}
|
|
85
85
|
SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS)
|
|
@@ -16,6 +16,10 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from typing import Dict, List, Union, get_args
|
|
18
18
|
|
|
19
|
+
from snowflake.cli._plugins.dcm.dcm_project_entity_model import (
|
|
20
|
+
DCMProjectEntity,
|
|
21
|
+
DCMProjectEntityModel,
|
|
22
|
+
)
|
|
19
23
|
from snowflake.cli._plugins.nativeapp.entities.application import (
|
|
20
24
|
ApplicationEntity,
|
|
21
25
|
ApplicationEntityModel,
|
|
@@ -26,10 +30,6 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import (
|
|
|
26
30
|
)
|
|
27
31
|
from snowflake.cli._plugins.notebook.notebook_entity import NotebookEntity
|
|
28
32
|
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
|
-
)
|
|
33
33
|
from snowflake.cli._plugins.snowpark.snowpark_entity import (
|
|
34
34
|
FunctionEntity,
|
|
35
35
|
ProcedureEntity,
|
|
@@ -62,7 +62,7 @@ Entity = Union[
|
|
|
62
62
|
ApplicationPackageEntity,
|
|
63
63
|
StreamlitEntity,
|
|
64
64
|
ProcedureEntity,
|
|
65
|
-
|
|
65
|
+
DCMProjectEntity,
|
|
66
66
|
FunctionEntity,
|
|
67
67
|
ComputePoolEntity,
|
|
68
68
|
ImageRepositoryEntity,
|
|
@@ -79,7 +79,7 @@ EntityModel = Union[
|
|
|
79
79
|
ImageRepositoryEntityModel,
|
|
80
80
|
ServiceEntityModel,
|
|
81
81
|
NotebookEntityModel,
|
|
82
|
-
|
|
82
|
+
DCMProjectEntityModel,
|
|
83
83
|
]
|
|
84
84
|
|
|
85
85
|
ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
|
snowflake/cli/api/rest_api.py
CHANGED
|
@@ -98,6 +98,7 @@ class RestApi:
|
|
|
98
98
|
data=json.dumps(data if data else {}),
|
|
99
99
|
no_retry=True,
|
|
100
100
|
raise_raw_http_failure=True,
|
|
101
|
+
external_session_id=None, # workaround for connector 3.16 bug, to be removed SNOW-2226816
|
|
101
102
|
)
|
|
102
103
|
|
|
103
104
|
def _database_exists(self, db_name: str) -> bool:
|
snowflake/cli/api/stage_path.py
CHANGED
|
@@ -220,9 +220,13 @@ class StagePath:
|
|
|
220
220
|
return self._path.name
|
|
221
221
|
|
|
222
222
|
def is_dir(self) -> bool:
|
|
223
|
+
if Path(self.path).exists():
|
|
224
|
+
return Path(self.path).is_dir()
|
|
223
225
|
return "." not in self.name
|
|
224
226
|
|
|
225
227
|
def is_file(self) -> bool:
|
|
228
|
+
if Path(self.path).exists():
|
|
229
|
+
return Path(self.path).is_file()
|
|
226
230
|
return not self.is_dir()
|
|
227
231
|
|
|
228
232
|
@property
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from typing import Any, Callable
|
|
17
|
+
from typing import Any, Callable, Dict, List, Union
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def deep_merge_dicts(
|
|
@@ -71,3 +71,44 @@ def traverse(
|
|
|
71
71
|
else:
|
|
72
72
|
visit_action(element)
|
|
73
73
|
return update_action(element)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_NestedDict = Dict[str, Union[Any, "_NestedDict"]]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def remove_key_from_nested_dict_if_exists(
|
|
80
|
+
root_dict: _NestedDict, key_path: List[str]
|
|
81
|
+
) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Removes a key from a nested dictionary, if it exists.
|
|
84
|
+
Removes all parents that become empty.
|
|
85
|
+
|
|
86
|
+
:return: True if the key was removed, False if it did not exist.
|
|
87
|
+
:raises ValueError: If a key in the path, besides the last one, was present but did not point to a dictionary.
|
|
88
|
+
"""
|
|
89
|
+
path = [root_dict]
|
|
90
|
+
for key in key_path:
|
|
91
|
+
curr_dict = path[-1]
|
|
92
|
+
if key not in curr_dict:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
child_dict = curr_dict[key]
|
|
96
|
+
if not isinstance(child_dict, dict) and len(path) < len(key_path):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Expected a dictionary at key '{key}', but got {str(type(child_dict))}."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
path.append(child_dict)
|
|
102
|
+
|
|
103
|
+
# Remove the target node, and any parents that become empty
|
|
104
|
+
is_target = True
|
|
105
|
+
for curr_key, curr_dict, child_dict in zip(
|
|
106
|
+
reversed(key_path), reversed(path[:-1]), reversed(path[1:])
|
|
107
|
+
):
|
|
108
|
+
if is_target or len(child_dict) == 0:
|
|
109
|
+
del curr_dict[curr_key]
|
|
110
|
+
is_target = False
|
|
111
|
+
else:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowflake-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.11.0
|
|
4
4
|
Summary: Snowflake CLI
|
|
5
5
|
Project-URL: Source code, https://github.com/snowflakedb/snowflake-cli
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/snowflakedb/snowflake-cli/issues
|
|
@@ -217,58 +217,26 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
217
217
|
Classifier: Programming Language :: SQL
|
|
218
218
|
Classifier: Topic :: Database
|
|
219
219
|
Requires-Python: >=3.10
|
|
220
|
-
Requires-Dist: annotated-types==0.7.0
|
|
221
|
-
Requires-Dist: asn1crypto==1.5.1
|
|
222
|
-
Requires-Dist: boto3==1.39.2
|
|
223
|
-
Requires-Dist: botocore==1.39.2
|
|
224
|
-
Requires-Dist: certifi==2025.6.15
|
|
225
|
-
Requires-Dist: cffi==1.17.1
|
|
226
|
-
Requires-Dist: cfgv==3.4.0
|
|
227
|
-
Requires-Dist: charset-normalizer==3.4.2
|
|
228
220
|
Requires-Dist: click==8.1.8
|
|
229
|
-
Requires-Dist: cryptography==45.0.5
|
|
230
|
-
Requires-Dist: faker==37.4.0
|
|
231
|
-
Requires-Dist: filelock==3.18.0
|
|
232
|
-
Requires-Dist: gitdb==4.0.12
|
|
233
221
|
Requires-Dist: gitpython==3.1.44
|
|
234
|
-
Requires-Dist:
|
|
235
|
-
Requires-Dist: idna==3.10
|
|
236
|
-
Requires-Dist: iniconfig==2.1.0
|
|
222
|
+
Requires-Dist: id==1.5.0
|
|
237
223
|
Requires-Dist: jinja2==3.1.6
|
|
238
|
-
Requires-Dist: keyring==25.6.0
|
|
239
|
-
Requires-Dist: markdown-it-py==3.0.0
|
|
240
|
-
Requires-Dist: markupsafe==3.0.2
|
|
241
|
-
Requires-Dist: nodeenv==1.9.1
|
|
242
224
|
Requires-Dist: packaging
|
|
243
225
|
Requires-Dist: pip
|
|
244
|
-
Requires-Dist: platformdirs==4.3.8
|
|
245
226
|
Requires-Dist: pluggy==1.6.0
|
|
246
227
|
Requires-Dist: prompt-toolkit==3.0.51
|
|
247
|
-
Requires-Dist: pydantic-core==2.33.2
|
|
248
228
|
Requires-Dist: pydantic==2.11.7
|
|
249
|
-
Requires-Dist: pygments==2.19.2
|
|
250
|
-
Requires-Dist: pyjwt==2.10.1
|
|
251
|
-
Requires-Dist: pyopenssl==25.1.0
|
|
252
|
-
Requires-Dist: python-dateutil==2.9.0.post0
|
|
253
|
-
Requires-Dist: pytz==2025.2
|
|
254
229
|
Requires-Dist: pyyaml==6.0.2
|
|
255
230
|
Requires-Dist: requests==2.32.4
|
|
256
231
|
Requires-Dist: requirements-parser==0.13.0
|
|
257
232
|
Requires-Dist: rich==14.0.0
|
|
258
233
|
Requires-Dist: setuptools==80.8.0
|
|
259
|
-
Requires-Dist:
|
|
260
|
-
Requires-Dist: snowflake-
|
|
261
|
-
Requires-Dist: snowflake-core==1.5.1
|
|
234
|
+
Requires-Dist: snowflake-connector-python[secure-local-storage]==3.17.2
|
|
235
|
+
Requires-Dist: snowflake-core==1.6.0
|
|
262
236
|
Requires-Dist: snowflake-snowpark-python==1.33.0; python_version < '3.12'
|
|
263
|
-
Requires-Dist: sortedcontainers==2.4.0
|
|
264
237
|
Requires-Dist: tomlkit==0.13.3
|
|
265
238
|
Requires-Dist: typer==0.16.0
|
|
266
|
-
Requires-Dist: typing-extensions==4.14.0
|
|
267
|
-
Requires-Dist: typing-inspection==0.4.1
|
|
268
239
|
Requires-Dist: urllib3<2.6,>=1.24.3
|
|
269
|
-
Requires-Dist: virtualenv==20.31.2
|
|
270
|
-
Requires-Dist: wcwidth==0.2.13
|
|
271
|
-
Requires-Dist: werkzeug==3.1.3
|
|
272
240
|
Provides-Extra: development
|
|
273
241
|
Requires-Dist: coverage==7.8.0; extra == 'development'
|
|
274
242
|
Requires-Dist: factory-boy==3.3.3; extra == 'development'
|
|
@@ -278,6 +246,7 @@ Requires-Dist: pytest-httpserver==1.1.3; extra == 'development'
|
|
|
278
246
|
Requires-Dist: pytest-randomly==3.16.0; extra == 'development'
|
|
279
247
|
Requires-Dist: pytest==8.4.1; extra == 'development'
|
|
280
248
|
Requires-Dist: syrupy==4.9.1; extra == 'development'
|
|
249
|
+
Requires-Dist: uv>0.8.0; extra == 'development'
|
|
281
250
|
Provides-Extra: packaging
|
|
282
251
|
Description-Content-Type: text/markdown
|
|
283
252
|
|
|
@@ -320,15 +289,20 @@ Feel free to file an issue or submit a PR here for general cases. For official s
|
|
|
320
289
|
|
|
321
290
|
## Install Snowflake CLI
|
|
322
291
|
|
|
323
|
-
### Install with
|
|
292
|
+
### Install with uv (PyPi)
|
|
324
293
|
|
|
325
|
-
We recommend installing Snowflake CLI in isolated environment using [
|
|
294
|
+
We recommend installing Snowflake CLI in an isolated environment using [uv](https://docs.astral.sh/uv/guides/tools/#installing-tools). Requires Python >= 3.10
|
|
326
295
|
|
|
327
296
|
```bash
|
|
328
|
-
|
|
297
|
+
uv tool install snowflake-cli
|
|
329
298
|
snow --help
|
|
330
299
|
```
|
|
331
300
|
|
|
301
|
+
Or, with a single command
|
|
302
|
+
```bash
|
|
303
|
+
uvx --from snowflake-cli snow --help
|
|
304
|
+
```
|
|
305
|
+
|
|
332
306
|
### Install with Homebrew (Mac only)
|
|
333
307
|
|
|
334
308
|
Requires [Homebrew](https://brew.sh/).
|