snowflake-cli 3.8.3__py3-none-any.whl → 3.9.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/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/version_check.py +14 -4
- snowflake/cli/_plugins/cortex/commands.py +34 -8
- snowflake/cli/_plugins/cortex/constants.py +2 -1
- snowflake/cli/_plugins/cortex/manager.py +81 -21
- snowflake/cli/_plugins/dbt/__init__.py +13 -0
- snowflake/cli/_plugins/dbt/commands.py +187 -0
- snowflake/cli/_plugins/dbt/constants.py +41 -0
- snowflake/cli/_plugins/dbt/manager.py +182 -0
- snowflake/cli/_plugins/dbt/plugin_spec.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +4 -1
- snowflake/cli/_plugins/project/commands.py +26 -3
- snowflake/cli/_plugins/project/manager.py +6 -1
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +1 -1
- snowflake/cli/_plugins/stage/commands.py +14 -3
- snowflake/cli/_plugins/stage/manager.py +39 -19
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +19 -30
- snowflake/cli/api/commands/snow_typer.py +3 -0
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/entities/common.py +0 -52
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/rest_api.py +3 -2
- snowflake/cli/api/secure_path.py +9 -0
- snowflake/cli/api/sql_execution.py +12 -9
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/METADATA +3 -3
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/RECORD +30 -25
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.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
|
-
|
|
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,
|
|
@@ -57,6 +58,12 @@ version_flag = typer.Option(
|
|
|
57
58
|
variables_flag = variables_option(
|
|
58
59
|
'Variables for the execution context; for example: `-D "<key>=<value>"`.'
|
|
59
60
|
)
|
|
61
|
+
configuration_flag = typer.Option(
|
|
62
|
+
None,
|
|
63
|
+
"--configuration",
|
|
64
|
+
help="Configuration of the project to use. If not specified default configuration is used.",
|
|
65
|
+
show_default=False,
|
|
66
|
+
)
|
|
60
67
|
from_option = OverrideableOption(
|
|
61
68
|
None,
|
|
62
69
|
"--from",
|
|
@@ -82,13 +89,17 @@ def execute(
|
|
|
82
89
|
identifier: FQN = project_identifier,
|
|
83
90
|
version: Optional[str] = version_flag,
|
|
84
91
|
variables: Optional[List[str]] = variables_flag,
|
|
92
|
+
configuration: Optional[str] = configuration_flag,
|
|
85
93
|
**options,
|
|
86
94
|
):
|
|
87
95
|
"""
|
|
88
96
|
Executes a project.
|
|
89
97
|
"""
|
|
90
98
|
result = ProjectManager().execute(
|
|
91
|
-
project_name=identifier,
|
|
99
|
+
project_name=identifier,
|
|
100
|
+
configuration=configuration,
|
|
101
|
+
version=version,
|
|
102
|
+
variables=variables,
|
|
92
103
|
)
|
|
93
104
|
return SingleQueryResult(result)
|
|
94
105
|
|
|
@@ -98,13 +109,18 @@ def dry_run(
|
|
|
98
109
|
identifier: FQN = project_identifier,
|
|
99
110
|
version: Optional[str] = version_flag,
|
|
100
111
|
variables: Optional[List[str]] = variables_flag,
|
|
112
|
+
configuration: Optional[str] = configuration_flag,
|
|
101
113
|
**options,
|
|
102
114
|
):
|
|
103
115
|
"""
|
|
104
116
|
Validates a project.
|
|
105
117
|
"""
|
|
106
118
|
result = ProjectManager().execute(
|
|
107
|
-
project_name=identifier,
|
|
119
|
+
project_name=identifier,
|
|
120
|
+
configuration=configuration,
|
|
121
|
+
version=version,
|
|
122
|
+
dry_run=True,
|
|
123
|
+
variables=variables,
|
|
108
124
|
)
|
|
109
125
|
return SingleQueryResult(result)
|
|
110
126
|
|
|
@@ -118,6 +134,9 @@ def create(
|
|
|
118
134
|
"--no-version",
|
|
119
135
|
help="Do not initialize project with a new version, only create the snowflake object.",
|
|
120
136
|
),
|
|
137
|
+
if_not_exists: bool = IfNotExistsOption(
|
|
138
|
+
help="Do nothing if the project already exists."
|
|
139
|
+
),
|
|
121
140
|
**options,
|
|
122
141
|
):
|
|
123
142
|
"""
|
|
@@ -133,7 +152,11 @@ def create(
|
|
|
133
152
|
)
|
|
134
153
|
om = ObjectManager()
|
|
135
154
|
if om.object_exists(object_type="project", fqn=project.fqn):
|
|
136
|
-
|
|
155
|
+
message = f"Project '{project.fqn}' already exists."
|
|
156
|
+
if if_not_exists:
|
|
157
|
+
return MessageResult(message)
|
|
158
|
+
raise CliError(message)
|
|
159
|
+
|
|
137
160
|
if not no_version and om.object_exists(
|
|
138
161
|
object_type="stage", fqn=FQN.from_stage(project.stage)
|
|
139
162
|
):
|
|
@@ -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:
|
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
|
|
402
|
-
temp_dir_with_copy = Path(tmp)
|
|
399
|
+
temp_dir_with_copy = Path(tmp_dir)
|
|
403
400
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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,
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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:
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -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
|
|
|
@@ -4,18 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Generic, List, Optional, Type, TypeVar, get_args
|
|
5
5
|
|
|
6
6
|
from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
|
|
7
|
-
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
8
7
|
from snowflake.cli.api.cli_global_context import get_cli_context, span
|
|
9
8
|
from snowflake.cli.api.entities.resolver import Dependency, DependencyResolver
|
|
10
9
|
from snowflake.cli.api.entities.utils import EntityActions, get_sql_executor
|
|
11
10
|
from snowflake.cli.api.identifiers import FQN
|
|
12
11
|
from snowflake.cli.api.sql_execution import SqlExecutor
|
|
13
|
-
from snowflake.cli.api.utils.path_utils import change_directory
|
|
14
|
-
from snowflake.cli.api.utils.python_api_utils import StageEncryptionType
|
|
15
12
|
from snowflake.connector import SnowflakeConnection
|
|
16
13
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
17
|
-
from snowflake.core import CreateMode
|
|
18
|
-
from snowflake.core.stage import Stage, StageEncryption, StageResource
|
|
19
14
|
|
|
20
15
|
T = TypeVar("T")
|
|
21
16
|
|
|
@@ -129,13 +124,6 @@ class EntityBase(Generic[T]):
|
|
|
129
124
|
raise ValueError("snow_api_root is not set")
|
|
130
125
|
return root
|
|
131
126
|
|
|
132
|
-
@property
|
|
133
|
-
def stage_object(self) -> "StageResource":
|
|
134
|
-
if self._stage_object is None:
|
|
135
|
-
self._stage_object = self._create_stage_if_not_exists()
|
|
136
|
-
|
|
137
|
-
return self._stage_object
|
|
138
|
-
|
|
139
127
|
@property
|
|
140
128
|
def model(self) -> T:
|
|
141
129
|
return self._entity_model
|
|
@@ -152,22 +140,6 @@ class EntityBase(Generic[T]):
|
|
|
152
140
|
def get_drop_sql(self) -> str:
|
|
153
141
|
return f"DROP {self.model.type.upper()} {self.identifier};" # type: ignore[attr-defined]
|
|
154
142
|
|
|
155
|
-
def _create_stage_if_not_exists(
|
|
156
|
-
self, stage_name: Optional[str] = None
|
|
157
|
-
) -> StageResource:
|
|
158
|
-
if stage_name is None:
|
|
159
|
-
stage_name = self.model.stage # type: ignore[attr-defined]
|
|
160
|
-
|
|
161
|
-
stage_collection = (
|
|
162
|
-
self.snow_api_root.databases[self.database].schemas[self.schema].stages # type: ignore[attr-defined]
|
|
163
|
-
)
|
|
164
|
-
stage_object = Stage(
|
|
165
|
-
name=stage_name,
|
|
166
|
-
encryption=StageEncryption(type=StageEncryptionType.SNOWFLAKE_SSE.value),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
return stage_collection.create(stage_object, mode=CreateMode.if_not_exists)
|
|
170
|
-
|
|
171
143
|
def _get_identifier(
|
|
172
144
|
self, schema: Optional[str] = None, database: Optional[str] = None
|
|
173
145
|
) -> str:
|
|
@@ -175,30 +147,6 @@ class EntityBase(Generic[T]):
|
|
|
175
147
|
db_to_use = database or self._entity_model.fqn.database or self._conn.database # type: ignore
|
|
176
148
|
return f"{self._entity_model.fqn.set_schema(schema_to_use).set_database(db_to_use).sql_identifier}" # type: ignore
|
|
177
149
|
|
|
178
|
-
def _upload_files_to_stage(
|
|
179
|
-
self,
|
|
180
|
-
stage: StageResource,
|
|
181
|
-
bundle_map: BundleMap,
|
|
182
|
-
stage_root: Optional[str] = None,
|
|
183
|
-
) -> None:
|
|
184
|
-
with change_directory(self.root):
|
|
185
|
-
for src, dest in bundle_map.all_mappings(
|
|
186
|
-
absolute=True, expand_directories=True
|
|
187
|
-
):
|
|
188
|
-
if src.is_file():
|
|
189
|
-
upload_dst = (
|
|
190
|
-
f"{stage_root}/{dest.relative_to(self.root)}"
|
|
191
|
-
if stage_root
|
|
192
|
-
else f"/{self.fqn.name}/{get_parent_path_for_stage_deployment(dest.relative_to(bundle_map.deploy_root()))}"
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
stage.put(
|
|
196
|
-
local_file_name=src.relative_to(self.root),
|
|
197
|
-
stage_location=upload_dst,
|
|
198
|
-
overwrite=True,
|
|
199
|
-
auto_compress=False,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
150
|
def get_from_fqn_or_conn(self, attribute_name: str) -> str:
|
|
203
151
|
attribute = getattr(self.fqn, attribute_name, None) or getattr(
|
|
204
152
|
self._conn, attribute_name, None
|
|
@@ -68,6 +68,7 @@ class FeatureFlag(FeatureFlagMixin):
|
|
|
68
68
|
)
|
|
69
69
|
ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
|
|
70
70
|
ENABLE_SPCS_SERVICE_EVENTS = BooleanFlag("ENABLE_SPCS_SERVICE_EVENTS", False)
|
|
71
|
+
ENABLE_DBT = BooleanFlag("ENABLE_DBT", False)
|
|
71
72
|
ENABLE_AUTH_KEYPAIR = BooleanFlag("ENABLE_AUTH_KEYPAIR", False)
|
|
72
73
|
ENABLE_NATIVE_APP_PYTHON_SETUP = BooleanFlag(
|
|
73
74
|
"ENABLE_NATIVE_APP_PYTHON_SETUP", False
|