snowflake-cli 3.8.3__py3-none-any.whl → 3.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  3. snowflake/cli/_app/version_check.py +14 -4
  4. snowflake/cli/_plugins/cortex/commands.py +34 -8
  5. snowflake/cli/_plugins/cortex/constants.py +2 -1
  6. snowflake/cli/_plugins/cortex/manager.py +81 -21
  7. snowflake/cli/_plugins/dbt/__init__.py +13 -0
  8. snowflake/cli/_plugins/dbt/commands.py +187 -0
  9. snowflake/cli/_plugins/dbt/constants.py +41 -0
  10. snowflake/cli/_plugins/dbt/manager.py +182 -0
  11. snowflake/cli/_plugins/dbt/plugin_spec.py +30 -0
  12. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +4 -1
  13. snowflake/cli/_plugins/project/commands.py +33 -6
  14. snowflake/cli/_plugins/project/manager.py +6 -1
  15. snowflake/cli/_plugins/snowpark/snowpark_entity.py +1 -1
  16. snowflake/cli/_plugins/spcs/image_registry/commands.py +2 -2
  17. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  18. snowflake/cli/_plugins/stage/commands.py +14 -3
  19. snowflake/cli/_plugins/stage/manager.py +39 -19
  20. snowflake/cli/_plugins/streamlit/streamlit_entity.py +19 -30
  21. snowflake/cli/api/commands/snow_typer.py +3 -0
  22. snowflake/cli/api/constants.py +2 -0
  23. snowflake/cli/api/entities/common.py +0 -52
  24. snowflake/cli/api/feature_flags.py +1 -0
  25. snowflake/cli/api/rest_api.py +3 -2
  26. snowflake/cli/api/secure_path.py +9 -0
  27. snowflake/cli/api/sql_execution.py +12 -9
  28. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/METADATA +3 -3
  29. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/RECORD +32 -27
  30. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/WHEEL +0 -0
  31. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/entry_points.txt +0 -0
  32. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,182 @@
1
+ # Copyright (c) 2025 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 __future__ import annotations
16
+
17
+ from collections import defaultdict
18
+ from pathlib import Path
19
+ from tempfile import TemporaryDirectory
20
+
21
+ import yaml
22
+ from snowflake.cli._plugins.dbt.constants import PROFILES_FILENAME
23
+ from snowflake.cli._plugins.object.manager import ObjectManager
24
+ from snowflake.cli._plugins.stage.manager import StageManager
25
+ from snowflake.cli.api.console import cli_console
26
+ from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
27
+ from snowflake.cli.api.exceptions import CliError
28
+ from snowflake.cli.api.identifiers import FQN
29
+ from snowflake.cli.api.secure_path import SecurePath
30
+ from snowflake.cli.api.sql_execution import SqlExecutionMixin
31
+ from snowflake.connector.cursor import SnowflakeCursor
32
+
33
+
34
+ class DBTManager(SqlExecutionMixin):
35
+ def list(self) -> SnowflakeCursor: # noqa: A003
36
+ query = "SHOW DBT PROJECTS"
37
+ return self.execute_query(query)
38
+
39
+ @staticmethod
40
+ def exists(name: FQN) -> bool:
41
+ return ObjectManager().object_exists(
42
+ object_type=ObjectType.DBT_PROJECT.value.cli_name, fqn=name
43
+ )
44
+
45
+ def deploy(
46
+ self,
47
+ name: FQN,
48
+ path: SecurePath,
49
+ profiles_path: SecurePath,
50
+ force: bool,
51
+ ) -> SnowflakeCursor:
52
+ dbt_project_path = path / "dbt_project.yml"
53
+ if not dbt_project_path.exists():
54
+ raise CliError(
55
+ f"dbt_project.yml does not exist in directory {path.path.absolute()}."
56
+ )
57
+
58
+ with dbt_project_path.open(read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as fd:
59
+ dbt_project = yaml.safe_load(fd)
60
+ try:
61
+ profile = dbt_project["profile"]
62
+ except KeyError:
63
+ raise CliError("`profile` is not defined in dbt_project.yml")
64
+
65
+ self._validate_profiles(profiles_path, profile)
66
+
67
+ with cli_console.phase("Creating temporary stage"):
68
+ stage_manager = StageManager()
69
+ stage_fqn = FQN.from_string(f"dbt_{name}_stage").using_context()
70
+ stage_name = stage_manager.get_standard_stage_prefix(stage_fqn)
71
+ stage_manager.create(stage_fqn, temporary=True)
72
+
73
+ with cli_console.phase("Copying project files to stage"):
74
+ with TemporaryDirectory() as tmp:
75
+ tmp_path = Path(tmp)
76
+ stage_manager.copy_to_tmp_dir(path.path, tmp_path)
77
+ self._prepare_profiles_file(profiles_path.path, tmp_path)
78
+ result_count = len(
79
+ list(
80
+ stage_manager.put_recursive(
81
+ path.path, stage_name, temp_directory=tmp_path
82
+ )
83
+ )
84
+ )
85
+ cli_console.step(f"Copied {result_count} files")
86
+
87
+ with cli_console.phase("Creating DBT project"):
88
+ if force is True:
89
+ query = f"CREATE OR REPLACE DBT PROJECT {name}"
90
+ elif self.exists(name=name):
91
+ query = f"ALTER DBT PROJECT {name} ADD VERSION"
92
+ else:
93
+ query = f"CREATE DBT PROJECT {name}"
94
+ query += f"\nFROM {stage_name}"
95
+ return self.execute_query(query)
96
+
97
+ @staticmethod
98
+ def _validate_profiles(profiles_path: SecurePath, target_profile: str) -> None:
99
+ """
100
+ Validates that:
101
+ * profiles.yml exists
102
+ * contain profile specified in dbt_project.yml
103
+ * no other profiles are defined there
104
+ * does not contain any confidential data like passwords
105
+ """
106
+ profiles_file = profiles_path / PROFILES_FILENAME
107
+ if not profiles_file.exists():
108
+ raise CliError(
109
+ f"{PROFILES_FILENAME} does not exist in directory {profiles_path.path.absolute()}."
110
+ )
111
+ with profiles_file.open(read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as fd:
112
+ profiles = yaml.safe_load(fd)
113
+
114
+ if target_profile not in profiles:
115
+ raise CliError(
116
+ f"profile {target_profile} is not defined in {PROFILES_FILENAME}"
117
+ )
118
+
119
+ errors = defaultdict(list)
120
+ if len(profiles.keys()) > 1:
121
+ for profile_name in profiles.keys():
122
+ if profile_name.lower() != target_profile.lower():
123
+ errors[profile_name].append("Remove unnecessary profiles")
124
+
125
+ required_fields = {
126
+ "account",
127
+ "database",
128
+ "role",
129
+ "schema",
130
+ "type",
131
+ "user",
132
+ "warehouse",
133
+ }
134
+ supported_fields = {
135
+ "threads",
136
+ }
137
+ for target_name, target in profiles[target_profile]["outputs"].items():
138
+ if missing_keys := required_fields - set(target.keys()):
139
+ errors[target_profile].append(
140
+ f"Missing required fields: {', '.join(sorted(missing_keys))} in target {target_name}"
141
+ )
142
+ if (
143
+ unsupported_keys := set(target.keys())
144
+ - required_fields
145
+ - supported_fields
146
+ ):
147
+ errors[target_profile].append(
148
+ f"Unsupported fields found: {', '.join(sorted(unsupported_keys))} in target {target_name}"
149
+ )
150
+ if "type" in target and target["type"].lower() != "snowflake":
151
+ errors[target_profile].append(
152
+ f"Value for type field is invalid. Should be set to `snowflake` in target {target_name}"
153
+ )
154
+
155
+ if errors:
156
+ message = f"Found following errors in {PROFILES_FILENAME}. Please fix them before proceeding:"
157
+ for target, issues in errors.items():
158
+ message += f"\n{target}"
159
+ message += "\n * " + "\n * ".join(issues)
160
+ raise CliError(message)
161
+
162
+ @staticmethod
163
+ def _prepare_profiles_file(profiles_path: Path, tmp_path: Path):
164
+ # We need to copy profiles.yml file (not symlink) in order to redact
165
+ # any comments without changing original file. This can be achieved
166
+ # with pyyaml, which looses comments while reading a yaml file
167
+ source_profiles_file = SecurePath(profiles_path / PROFILES_FILENAME)
168
+ target_profiles_file = SecurePath(tmp_path / PROFILES_FILENAME)
169
+ if target_profiles_file.exists():
170
+ target_profiles_file.unlink()
171
+ with source_profiles_file.open(
172
+ read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
173
+ ) as sfd, target_profiles_file.open(mode="w") as tfd:
174
+ yaml.safe_dump(yaml.safe_load(sfd), tfd)
175
+
176
+ def execute(
177
+ self, dbt_command: str, name: str, run_async: bool, *dbt_cli_args
178
+ ) -> SnowflakeCursor:
179
+ if dbt_cli_args:
180
+ dbt_command = " ".join([dbt_command, *dbt_cli_args]).strip()
181
+ query = f"EXECUTE DBT PROJECT {name} args='{dbt_command}'"
182
+ return self.execute_query(query, _exec_async=run_async)
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2025 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 snowflake.cli._plugins.dbt import commands
16
+ from snowflake.cli.api.plugins.command import (
17
+ SNOWCLI_ROOT_COMMAND_PATH,
18
+ CommandSpec,
19
+ CommandType,
20
+ plugin_hook_impl,
21
+ )
22
+
23
+
24
+ @plugin_hook_impl
25
+ def command_spec():
26
+ return CommandSpec(
27
+ parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
28
+ command_type=CommandType.COMMAND_GROUP,
29
+ typer_instance=commands.app.create_instance(),
30
+ )
@@ -159,7 +159,10 @@ class SnowflakeSQLFacade:
159
159
  )
160
160
 
161
161
  try:
162
- prev_obj = to_identifier(current_obj_result_row[0])
162
+ if current_obj_result_row[0]:
163
+ prev_obj = to_identifier(current_obj_result_row[0])
164
+ else:
165
+ prev_obj = None
163
166
  except IndexError:
164
167
  prev_obj = None
165
168
 
@@ -26,6 +26,7 @@ from snowflake.cli._plugins.project.project_entity_model import (
26
26
  from snowflake.cli.api.cli_global_context import get_cli_context
27
27
  from snowflake.cli.api.commands.decorators import with_project_definition
28
28
  from snowflake.cli.api.commands.flags import (
29
+ IfNotExistsOption,
29
30
  OverrideableOption,
30
31
  PruneOption,
31
32
  entity_argument,
@@ -39,7 +40,11 @@ from snowflake.cli.api.console.console import cli_console
39
40
  from snowflake.cli.api.constants import ObjectType
40
41
  from snowflake.cli.api.exceptions import CliError
41
42
  from snowflake.cli.api.identifiers import FQN
42
- from snowflake.cli.api.output.types import MessageResult, QueryResult, SingleQueryResult
43
+ from snowflake.cli.api.output.types import (
44
+ MessageResult,
45
+ QueryJsonValueResult,
46
+ QueryResult,
47
+ )
43
48
 
44
49
  app = SnowTyperFactory(
45
50
  name="project",
@@ -57,6 +62,12 @@ version_flag = typer.Option(
57
62
  variables_flag = variables_option(
58
63
  'Variables for the execution context; for example: `-D "<key>=<value>"`.'
59
64
  )
65
+ configuration_flag = typer.Option(
66
+ None,
67
+ "--configuration",
68
+ help="Configuration of the project to use. If not specified default configuration is used.",
69
+ show_default=False,
70
+ )
60
71
  from_option = OverrideableOption(
61
72
  None,
62
73
  "--from",
@@ -82,15 +93,19 @@ def execute(
82
93
  identifier: FQN = project_identifier,
83
94
  version: Optional[str] = version_flag,
84
95
  variables: Optional[List[str]] = variables_flag,
96
+ configuration: Optional[str] = configuration_flag,
85
97
  **options,
86
98
  ):
87
99
  """
88
100
  Executes a project.
89
101
  """
90
102
  result = ProjectManager().execute(
91
- project_name=identifier, version=version, variables=variables
103
+ project_name=identifier,
104
+ configuration=configuration,
105
+ version=version,
106
+ variables=variables,
92
107
  )
93
- return SingleQueryResult(result)
108
+ return QueryJsonValueResult(result)
94
109
 
95
110
 
96
111
  @app.command(requires_connection=True)
@@ -98,15 +113,20 @@ def dry_run(
98
113
  identifier: FQN = project_identifier,
99
114
  version: Optional[str] = version_flag,
100
115
  variables: Optional[List[str]] = variables_flag,
116
+ configuration: Optional[str] = configuration_flag,
101
117
  **options,
102
118
  ):
103
119
  """
104
120
  Validates a project.
105
121
  """
106
122
  result = ProjectManager().execute(
107
- project_name=identifier, version=version, dry_run=True, variables=variables
123
+ project_name=identifier,
124
+ configuration=configuration,
125
+ version=version,
126
+ dry_run=True,
127
+ variables=variables,
108
128
  )
109
- return SingleQueryResult(result)
129
+ return QueryJsonValueResult(result)
110
130
 
111
131
 
112
132
  @app.command(requires_connection=True)
@@ -118,6 +138,9 @@ def create(
118
138
  "--no-version",
119
139
  help="Do not initialize project with a new version, only create the snowflake object.",
120
140
  ),
141
+ if_not_exists: bool = IfNotExistsOption(
142
+ help="Do nothing if the project already exists."
143
+ ),
121
144
  **options,
122
145
  ):
123
146
  """
@@ -133,7 +156,11 @@ def create(
133
156
  )
134
157
  om = ObjectManager()
135
158
  if om.object_exists(object_type="project", fqn=project.fqn):
136
- raise CliError(f"Project '{project.fqn}' already exists.")
159
+ message = f"Project '{project.fqn}' already exists."
160
+ if if_not_exists:
161
+ return MessageResult(message)
162
+ raise CliError(message)
163
+
137
164
  if not no_version and om.object_exists(
138
165
  object_type="stage", fqn=FQN.from_stage(project.stage)
139
166
  ):
@@ -31,15 +31,20 @@ class ProjectManager(SqlExecutionMixin):
31
31
  def execute(
32
32
  self,
33
33
  project_name: FQN,
34
+ configuration: str | None = None,
34
35
  version: str | None = None,
35
36
  variables: List[str] | None = None,
36
37
  dry_run: bool = False,
37
38
  ):
38
39
  query = f"EXECUTE PROJECT {project_name.sql_identifier}"
40
+ if configuration or variables:
41
+ query += f" USING"
42
+ if configuration:
43
+ query += f" CONFIGURATION {configuration}"
39
44
  if variables:
40
45
  query += StageManager.parse_execute_variables(
41
46
  parse_key_value_variables(variables)
42
- )
47
+ ).removeprefix(" using")
43
48
  if version:
44
49
  query += f" WITH VERSION {version}"
45
50
  if dry_run:
@@ -242,7 +242,7 @@ class SnowparkEntity(EntityBase[Generic[T]]):
242
242
  )
243
243
 
244
244
  zip_dir(
245
- source=tmp_dir,
245
+ source=tmp_dir.path,
246
246
  dest_zip=bundle_dir / archive_name,
247
247
  )
248
248
 
@@ -64,10 +64,10 @@ def url(private_link: bool = PrivateLinkOption, **options) -> MessageResult:
64
64
 
65
65
 
66
66
  @app.command(requires_connection=True)
67
- def login(**options) -> MessageResult:
67
+ def login(private_link: bool = PrivateLinkOption, **options) -> MessageResult:
68
68
  """
69
69
  Logs in to the account image registry with the current user's credentials through Docker.
70
70
 
71
71
  Must be called from a role that can view at least one image repository in the image registry.
72
72
  """
73
- return MessageResult(RegistryManager().docker_registry_login().strip())
73
+ return MessageResult(RegistryManager().docker_registry_login(private_link).strip())
@@ -101,8 +101,8 @@ class RegistryManager(SqlExecutionMixin):
101
101
  sample_repository_url = f"//{sample_repository_url}"
102
102
  return urlparse(sample_repository_url).netloc
103
103
 
104
- def docker_registry_login(self) -> str:
105
- registry_url = self.get_registry_url()
104
+ def docker_registry_login(self, private_link: bool = False) -> str:
105
+ registry_url = self.get_registry_url(private_link)
106
106
  token = self.get_token()
107
107
  command = [
108
108
  "docker",
@@ -29,7 +29,10 @@ from snowflake.cli._plugins.stage.diff import (
29
29
  DiffResult,
30
30
  compute_stage_diff,
31
31
  )
32
- from snowflake.cli._plugins.stage.manager import StageManager
32
+ from snowflake.cli._plugins.stage.manager import (
33
+ InternalStageEncryptionType,
34
+ StageManager,
35
+ )
33
36
  from snowflake.cli._plugins.stage.utils import print_diff_to_console
34
37
  from snowflake.cli.api.cli_global_context import get_cli_context
35
38
  from snowflake.cli.api.commands.common import OnErrorType
@@ -150,11 +153,19 @@ def copy(
150
153
 
151
154
 
152
155
  @app.command("create", requires_connection=True)
153
- def stage_create(stage_name: FQN = StageNameArgument, **options) -> CommandResult:
156
+ def stage_create(
157
+ stage_name: FQN = StageNameArgument,
158
+ encryption: InternalStageEncryptionType = typer.Option(
159
+ InternalStageEncryptionType.SNOWFLAKE_FULL.value,
160
+ "--encryption",
161
+ help="Type of encryption supported for all files stored on the stage.",
162
+ ),
163
+ **options,
164
+ ) -> CommandResult:
154
165
  """
155
166
  Creates a named stage if it does not already exist.
156
167
  """
157
- cursor = StageManager().create(fqn=stage_name)
168
+ cursor = StageManager().create(fqn=stage_name, encryption=encryption)
158
169
  return SingleQueryResult(cursor)
159
170
 
160
171
 
@@ -25,6 +25,7 @@ import time
25
25
  from collections import deque
26
26
  from contextlib import nullcontext
27
27
  from dataclasses import dataclass
28
+ from enum import Enum
28
29
  from os import path
29
30
  from pathlib import Path
30
31
  from tempfile import TemporaryDirectory
@@ -68,6 +69,11 @@ OMIT_FIRST = slice(1, None)
68
69
  STAGE_PATH_REGEX = rf"(?P<prefix>(@|{re.escape('snow://')}))?(?:(?P<first_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?:(?P<second_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?P<name>{VALID_IDENTIFIER_REGEX})/?(?P<directory>([^/]*/?)*)?"
69
70
 
70
71
 
72
+ class InternalStageEncryptionType(Enum):
73
+ SNOWFLAKE_FULL = "SNOWFLAKE_FULL"
74
+ SNOWFLAKE_SSE = "SNOWFLAKE_SSE"
75
+
76
+
71
77
  @dataclass
72
78
  class StagePathParts:
73
79
  directory: str
@@ -379,15 +385,7 @@ class StageManager(SqlExecutionMixin):
379
385
  else:
380
386
  dest_path.mkdir(exist_ok=True, parents=True)
381
387
 
382
- def put_recursive(
383
- self,
384
- local_path: Path,
385
- stage_path: str,
386
- parallel: int = 4,
387
- overwrite: bool = False,
388
- role: Optional[str] = None,
389
- auto_compress: bool = False,
390
- ) -> Generator[dict, None, None]:
388
+ def copy_to_tmp_dir(self, local_path: Path, tmp_dir: Path):
391
389
  if local_path.is_file():
392
390
  raise UsageError("Cannot use recursive upload with a single file.")
393
391
 
@@ -398,16 +396,32 @@ class StageManager(SqlExecutionMixin):
398
396
  root = Path([p for p in local_path.parents if p.is_dir()][0])
399
397
  glob_pattern = str(local_path)
400
398
 
401
- with TemporaryDirectory() as tmp:
402
- temp_dir_with_copy = Path(tmp)
399
+ temp_dir_with_copy = Path(tmp_dir)
403
400
 
404
- # Create a symlink or copy the file to the temp directory
405
- for file_or_dir in glob.iglob(glob_pattern, recursive=True):
406
- self._symlink_or_copy(
407
- source_root=root,
408
- source_file_or_dir=Path(file_or_dir),
409
- dest_dir=temp_dir_with_copy,
410
- )
401
+ # Create a symlink or copy the file to the temp directory
402
+ for file_or_dir in glob.iglob(glob_pattern, recursive=True):
403
+ self._symlink_or_copy(
404
+ source_root=root,
405
+ source_file_or_dir=Path(file_or_dir),
406
+ dest_dir=temp_dir_with_copy,
407
+ )
408
+
409
+ def put_recursive(
410
+ self,
411
+ local_path: Path,
412
+ stage_path: str,
413
+ parallel: int = 4,
414
+ overwrite: bool = False,
415
+ role: Optional[str] = None,
416
+ auto_compress: bool = False,
417
+ temp_directory: Optional[Path] = None,
418
+ ) -> Generator[dict, None, None]:
419
+ is_temp_dir_provided = temp_directory is not None
420
+
421
+ with TemporaryDirectory() if temp_directory is None else nullcontext(str(temp_directory)) as tmp: # type: ignore[attr-defined]
422
+ temp_dir_with_copy = Path(tmp)
423
+ if not is_temp_dir_provided:
424
+ self.copy_to_tmp_dir(local_path, temp_dir_with_copy)
411
425
 
412
426
  # Find the deepest directories, we will be iterating from bottom to top
413
427
  deepest_dirs_list = self._find_deepest_directories(temp_dir_with_copy)
@@ -515,10 +529,16 @@ class StageManager(SqlExecutionMixin):
515
529
  return self.execute_query(f"remove {stage_path.path_for_sql()}")
516
530
 
517
531
  def create(
518
- self, fqn: FQN, comment: Optional[str] = None, temporary: bool = False
532
+ self,
533
+ fqn: FQN,
534
+ comment: Optional[str] = None,
535
+ temporary: bool = False,
536
+ encryption: InternalStageEncryptionType | None = None,
519
537
  ) -> SnowflakeCursor:
520
538
  temporary_str = "temporary " if temporary else ""
521
539
  query = f"create {temporary_str}stage if not exists {fqn.sql_identifier}"
540
+ if encryption:
541
+ query += f" encryption = (type = '{encryption.value}')"
522
542
  if comment:
523
543
  query += f" comment='{comment}'"
524
544
  return self.execute_query(query)
@@ -99,9 +99,6 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
99
99
  f"Streamlit {self.model.fqn.sql_identifier} already exists. Use 'replace' option to overwrite."
100
100
  )
101
101
 
102
- console.step(f"Creating stage {self.model.stage} if not exists")
103
- stage = self._create_stage_if_not_exists()
104
-
105
102
  if (
106
103
  experimental
107
104
  or GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled()
@@ -115,23 +112,20 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
115
112
  name = (
116
113
  self.model.identifier.name
117
114
  if isinstance(self.model.identifier, Identifier)
118
- else self.model.identifier
115
+ else self.model.identifier or self.entity_id
119
116
  )
120
117
  stage_root = StageManager.get_standard_stage_prefix(
121
118
  f"{FQN.from_string(self.model.stage).using_connection(self._conn)}/{name}"
122
119
  )
123
- if prune:
124
- sync_deploy_root_with_stage(
125
- console=self._workspace_ctx.console,
126
- deploy_root=bundle_map.deploy_root(),
127
- bundle_map=bundle_map,
128
- prune=prune,
129
- recursive=True,
130
- stage_path=StageManager().stage_path_parts_from_str(stage_root),
131
- print_diff=True,
132
- )
133
- else:
134
- self._upload_files_to_stage(stage, bundle_map, None)
120
+ sync_deploy_root_with_stage(
121
+ console=self._workspace_ctx.console,
122
+ deploy_root=bundle_map.deploy_root(),
123
+ bundle_map=bundle_map,
124
+ prune=prune,
125
+ recursive=True,
126
+ stage_path=StageManager().stage_path_parts_from_str(stage_root),
127
+ print_diff=True,
128
+ )
135
129
 
136
130
  console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")
137
131
 
@@ -266,17 +260,12 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
266
260
  else:
267
261
  stage_root = f"{embeded_stage_name}/default_checkout"
268
262
 
269
- stage_resource = self._create_stage_if_not_exists(embeded_stage_name)
270
-
271
- if prune:
272
- sync_deploy_root_with_stage(
273
- console=self._workspace_ctx.console,
274
- deploy_root=bundle_map.deploy_root(),
275
- bundle_map=bundle_map,
276
- prune=prune,
277
- recursive=True,
278
- stage_path=StageManager().stage_path_parts_from_str(stage_root),
279
- print_diff=True,
280
- )
281
- else:
282
- self._upload_files_to_stage(stage_resource, bundle_map)
263
+ sync_deploy_root_with_stage(
264
+ console=self._workspace_ctx.console,
265
+ deploy_root=bundle_map.deploy_root(),
266
+ bundle_map=bundle_map,
267
+ prune=prune,
268
+ recursive=True,
269
+ stage_path=StageManager().stage_path_parts_from_str(stage_root),
270
+ print_diff=True,
271
+ )
@@ -228,6 +228,7 @@ class SnowTyperFactory:
228
228
  short_help: Optional[str] = None,
229
229
  is_hidden: Optional[Callable[[], bool]] = None,
230
230
  deprecated: bool = False,
231
+ subcommand_metavar: Optional[str] = None,
231
232
  ):
232
233
  self.name = name
233
234
  self.help = help
@@ -237,6 +238,7 @@ class SnowTyperFactory:
237
238
  self.commands_to_register: List[SnowTyperCommandData] = []
238
239
  self.subapps_to_register: List[SnowTyperFactory] = []
239
240
  self.callbacks_to_register: List[Callable] = []
241
+ self.subcommand_metavar = subcommand_metavar
240
242
 
241
243
  def create_instance(self) -> SnowTyper:
242
244
  app = SnowTyper(
@@ -245,6 +247,7 @@ class SnowTyperFactory:
245
247
  short_help=self.short_help,
246
248
  hidden=self.is_hidden() if self.is_hidden else False,
247
249
  deprecated=self.deprecated,
250
+ subcommand_metavar=self.subcommand_metavar,
248
251
  )
249
252
  # register commands
250
253
  for command in self.commands_to_register:
@@ -35,6 +35,7 @@ class ObjectNames:
35
35
 
36
36
  class ObjectType(Enum):
37
37
  COMPUTE_POOL = ObjectNames("compute-pool", "compute pool", "compute pools")
38
+ DBT_PROJECT = ObjectNames("dbt-project", "dbt project", "dbt projects")
38
39
  DATABASE = ObjectNames("database", "database", "databases")
39
40
  FUNCTION = ObjectNames("function", "function", "functions")
40
41
  INTEGRATION = ObjectNames("integration", "integration", "integrations")
@@ -79,6 +80,7 @@ UNSUPPORTED_OBJECTS = {
79
80
  ObjectType.APPLICATION.value.cli_name,
80
81
  ObjectType.APPLICATION_PACKAGE.value.cli_name,
81
82
  ObjectType.PROJECT.value.cli_name,
83
+ ObjectType.DBT_PROJECT.value.cli_name,
82
84
  }
83
85
  SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS)
84
86