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.
Files changed (41) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/auth/__init__.py +13 -0
  3. snowflake/cli/_app/auth/errors.py +28 -0
  4. snowflake/cli/_app/auth/oidc_providers.py +393 -0
  5. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -2
  6. snowflake/cli/_app/constants.py +10 -0
  7. snowflake/cli/_app/snow_connector.py +35 -0
  8. snowflake/cli/_plugins/auth/__init__.py +4 -2
  9. snowflake/cli/_plugins/auth/keypair/commands.py +2 -0
  10. snowflake/cli/_plugins/auth/oidc/__init__.py +13 -0
  11. snowflake/cli/_plugins/auth/oidc/commands.py +47 -0
  12. snowflake/cli/_plugins/auth/oidc/manager.py +66 -0
  13. snowflake/cli/_plugins/auth/oidc/plugin_spec.py +30 -0
  14. snowflake/cli/_plugins/connection/commands.py +37 -3
  15. snowflake/cli/_plugins/dbt/manager.py +7 -7
  16. snowflake/cli/_plugins/{project → dcm}/commands.py +113 -122
  17. snowflake/cli/_plugins/{project/project_entity_model.py → dcm/dcm_project_entity_model.py} +5 -5
  18. snowflake/cli/_plugins/dcm/manager.py +96 -0
  19. snowflake/cli/_plugins/{project → dcm}/plugin_spec.py +1 -1
  20. snowflake/cli/_plugins/notebook/notebook_entity.py +2 -0
  21. snowflake/cli/_plugins/notebook/notebook_entity_model.py +8 -1
  22. snowflake/cli/_plugins/object/command_aliases.py +16 -1
  23. snowflake/cli/_plugins/object/commands.py +27 -1
  24. snowflake/cli/_plugins/object/manager.py +12 -1
  25. snowflake/cli/_plugins/snowpark/commands.py +8 -1
  26. snowflake/cli/api/commands/decorators.py +7 -0
  27. snowflake/cli/api/commands/flags.py +26 -0
  28. snowflake/cli/api/config.py +24 -0
  29. snowflake/cli/api/connections.py +1 -0
  30. snowflake/cli/api/constants.py +2 -2
  31. snowflake/cli/api/project/schemas/entities/entities.py +6 -6
  32. snowflake/cli/api/rest_api.py +1 -0
  33. snowflake/cli/api/stage_path.py +4 -0
  34. snowflake/cli/api/utils/dict_utils.py +42 -1
  35. {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/METADATA +13 -39
  36. {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/RECORD +40 -33
  37. snowflake/cli/_plugins/project/manager.py +0 -134
  38. /snowflake/cli/_plugins/{project → dcm}/__init__.py +0 -0
  39. {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/WHEEL +0 -0
  40. {snowflake_cli-3.10.0.dist-info → snowflake_cli-3.11.0.dist-info}/entry_points.txt +0 -0
  41. {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.project import commands
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 and not self:
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(like: str = like_option, **options): # type: ignore
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(object_type=object_type, like=like, scope=scope)
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
- query = f"show {object_name}"
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(object_type=object_type.value, like=like, scope=scope, **options)
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",
@@ -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):
@@ -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
@@ -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.PROJECT.value.cli_name,
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
- ProjectEntity,
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
- ProjectEntityModel,
82
+ DCMProjectEntityModel,
83
83
  ]
84
84
 
85
85
  ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
@@ -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:
@@ -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.10.0
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: identify==2.6.12
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: shellingham==1.5.4
260
- Requires-Dist: snowflake-connector-python[secure-local-storage]==3.15.0
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 pipx (PyPi)
292
+ ### Install with uv (PyPi)
324
293
 
325
- We recommend installing Snowflake CLI in isolated environment using [pipx](https://pipx.pypa.io/stable/). Requires Python >= 3.10
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
- pipx install snowflake-cli
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/).