snowflake-cli 3.3.0__py3-none-any.whl → 3.5.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/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +220 -197
- snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +4 -30
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_plugins/connection/commands.py +2 -4
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/commands.py +21 -19
- snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
- snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
- snowflake/cli/_plugins/notebook/commands.py +54 -2
- snowflake/cli/_plugins/notebook/exceptions.py +1 -1
- snowflake/cli/_plugins/notebook/manager.py +3 -3
- snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
- snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
- snowflake/cli/_plugins/notebook/types.py +3 -0
- snowflake/cli/_plugins/plugin/commands.py +79 -0
- snowflake/cli/_plugins/plugin/manager.py +74 -0
- snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/__init__.py +0 -0
- snowflake/cli/_plugins/project/commands.py +157 -0
- snowflake/cli/_plugins/project/feature_flags.py +22 -0
- snowflake/cli/_plugins/project/manager.py +76 -0
- snowflake/cli/_plugins/project/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/project_entity_model.py +40 -0
- snowflake/cli/_plugins/snowpark/commands.py +49 -30
- snowflake/cli/_plugins/snowpark/common.py +47 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
- snowflake/cli/_plugins/snowpark/zipper.py +33 -1
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +53 -5
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +51 -1
- snowflake/cli/_plugins/spcs/services/manager.py +114 -0
- snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
- snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
- snowflake/cli/_plugins/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +26 -13
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +18 -24
- snowflake/cli/_plugins/streamlit/manager.py +37 -27
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
- snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
- snowflake/cli/_plugins/workspace/commands.py +3 -3
- snowflake/cli/_plugins/workspace/manager.py +1 -1
- snowflake/cli/api/artifacts/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +14 -1
- snowflake/cli/api/commands/flags.py +34 -13
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +30 -2
- snowflake/cli/api/config.py +15 -10
- snowflake/cli/api/constants.py +1 -0
- snowflake/cli/api/entities/common.py +14 -32
- snowflake/cli/api/entities/resolver.py +160 -0
- snowflake/cli/api/entities/utils.py +56 -15
- snowflake/cli/api/errno.py +3 -0
- snowflake/cli/api/exceptions.py +8 -1
- snowflake/cli/api/feature_flags.py +1 -1
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_conversion.py +3 -2
- snowflake/cli/api/project/definition_helper.py +31 -0
- snowflake/cli/api/project/project_paths.py +28 -0
- snowflake/cli/api/project/schemas/entities/common.py +130 -1
- snowflake/cli/api/project/schemas/entities/entities.py +30 -0
- snowflake/cli/api/project/schemas/project_definition.py +27 -0
- snowflake/cli/api/project/schemas/updatable_model.py +2 -2
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
- snowflake/cli/api/secure_path.py +6 -0
- snowflake/cli/api/sql_execution.py +5 -1
- snowflake/cli/api/stage_path.py +7 -2
- snowflake/cli/api/utils/graph.py +3 -0
- snowflake/cli/api/utils/path_utils.py +24 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +12 -13
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +109 -85
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- /snowflake/cli/{_app/api_impl/plugin → api/artifacts}/__init__.py +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,38 +13,60 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import glob
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
16
19
|
from dataclasses import dataclass
|
|
17
20
|
from pathlib import Path, PurePosixPath
|
|
21
|
+
from typing import Optional
|
|
18
22
|
|
|
19
|
-
from snowflake.cli._plugins.snowpark.snowpark_entity_model import PathMapping
|
|
20
23
|
from snowflake.cli._plugins.snowpark.zipper import zip_dir
|
|
21
24
|
from snowflake.cli.api.console import cli_console
|
|
22
25
|
from snowflake.cli.api.constants import DEPLOYMENT_STAGE
|
|
26
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
23
27
|
from snowflake.cli.api.identifiers import FQN
|
|
28
|
+
from snowflake.cli.api.project.project_paths import ProjectPaths, bundle_root
|
|
29
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
24
30
|
from snowflake.cli.api.secure_path import SecurePath
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
@dataclass
|
|
28
|
-
class SnowparkProjectPaths:
|
|
34
|
+
class SnowparkProjectPaths(ProjectPaths):
|
|
29
35
|
"""
|
|
30
|
-
This class
|
|
36
|
+
This class allows you to manage files paths related to given project.
|
|
31
37
|
"""
|
|
32
38
|
|
|
33
|
-
project_root: Path
|
|
34
|
-
|
|
35
39
|
def path_relative_to_root(self, artifact_path: Path) -> Path:
|
|
36
40
|
if artifact_path.is_absolute():
|
|
37
41
|
return artifact_path
|
|
38
42
|
return (self.project_root / artifact_path).resolve()
|
|
39
43
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
def get_artifact_dto(self, artifact_path: PathMapping) -> Artifact:
|
|
45
|
+
if FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_enabled():
|
|
46
|
+
return Artifact(
|
|
47
|
+
project_root=self.project_root,
|
|
48
|
+
bundle_root=self.bundle_root,
|
|
49
|
+
dest=artifact_path.dest,
|
|
50
|
+
path=Path(artifact_path.src),
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
return ArtifactOldBuild(
|
|
54
|
+
dest=artifact_path.dest,
|
|
55
|
+
path=self.path_relative_to_root(Path(artifact_path.src)),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get_dependencies_artifact(self) -> Artifact:
|
|
59
|
+
if FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_enabled():
|
|
60
|
+
return Artifact(
|
|
61
|
+
project_root=self.project_root,
|
|
62
|
+
bundle_root=self.bundle_root,
|
|
63
|
+
dest=None,
|
|
64
|
+
path=Path("dependencies.zip"),
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
return ArtifactOldBuild(
|
|
68
|
+
dest=None, path=self.path_relative_to_root(Path("dependencies.zip"))
|
|
69
|
+
)
|
|
48
70
|
|
|
49
71
|
@property
|
|
50
72
|
def snowflake_requirements(self) -> SecurePath:
|
|
@@ -57,19 +79,130 @@ class SnowparkProjectPaths:
|
|
|
57
79
|
return SecurePath(self.path_relative_to_root(Path("requirements.txt")))
|
|
58
80
|
|
|
59
81
|
@property
|
|
60
|
-
def
|
|
61
|
-
return self.
|
|
82
|
+
def bundle_root(self) -> Path:
|
|
83
|
+
return bundle_root(self.project_root, "snowpark")
|
|
62
84
|
|
|
63
85
|
|
|
64
86
|
@dataclass(unsafe_hash=True)
|
|
65
|
-
class
|
|
66
|
-
"""Helper for getting paths related to given
|
|
87
|
+
class Artifact:
|
|
88
|
+
"""Helper for getting paths related to given artifact."""
|
|
67
89
|
|
|
90
|
+
project_root: Path
|
|
91
|
+
bundle_root: Path
|
|
68
92
|
path: Path
|
|
69
93
|
dest: str | None = None
|
|
70
94
|
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
project_root: Path,
|
|
98
|
+
bundle_root: Path,
|
|
99
|
+
path: Path,
|
|
100
|
+
dest: Optional[str] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self.project_root = project_root
|
|
103
|
+
self.bundle_root = bundle_root
|
|
104
|
+
self.path = path
|
|
105
|
+
self.dest = dest
|
|
106
|
+
if self.dest and not self._is_dest_a_file() and not self.dest.endswith("/"):
|
|
107
|
+
self.dest = self.dest + "/"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def _artifact_name(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Returns artifact name. Directories are mapped to corresponding .zip files.
|
|
113
|
+
For paths with glob patterns, the last part of the path is used.
|
|
114
|
+
For files, the file name is used.
|
|
115
|
+
"""
|
|
116
|
+
if glob.has_magic(str(self.path)):
|
|
117
|
+
last_part = None
|
|
118
|
+
for part in self.path.parts:
|
|
119
|
+
if glob.has_magic(part):
|
|
120
|
+
break
|
|
121
|
+
else:
|
|
122
|
+
last_part = part
|
|
123
|
+
if not last_part:
|
|
124
|
+
last_part = os.path.commonpath(
|
|
125
|
+
[str(self.path), str(self.path.absolute())]
|
|
126
|
+
)
|
|
127
|
+
return last_part + ".zip"
|
|
128
|
+
if (self.project_root / self.path).is_dir():
|
|
129
|
+
return self.path.stem + ".zip"
|
|
130
|
+
if (self.project_root / self.path).is_file() and self._is_dest_a_file():
|
|
131
|
+
return Path(self.dest).name # type: ignore
|
|
132
|
+
return self.path.name
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def post_build_path(self) -> Path:
|
|
136
|
+
"""
|
|
137
|
+
Returns post-build artifact path. Directories are mapped to corresponding .zip files.
|
|
138
|
+
"""
|
|
139
|
+
bundle_root = self.bundle_root
|
|
140
|
+
path = (
|
|
141
|
+
self._path_until_asterisk()
|
|
142
|
+
if glob.has_magic(str(self.path))
|
|
143
|
+
else self.path.parent
|
|
144
|
+
)
|
|
145
|
+
if self._is_dest_a_file():
|
|
146
|
+
return bundle_root / self.dest # type: ignore
|
|
147
|
+
return bundle_root / (self.dest or path) / self._artifact_name
|
|
148
|
+
|
|
149
|
+
def upload_path(self, stage: FQN | str | None) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Path on stage to which the artifact should be uploaded.
|
|
152
|
+
"""
|
|
153
|
+
stage = stage or DEPLOYMENT_STAGE
|
|
154
|
+
if isinstance(stage, str):
|
|
155
|
+
stage = FQN.from_stage(stage).using_context()
|
|
156
|
+
|
|
157
|
+
stage_path = PurePosixPath(f"@{stage}")
|
|
158
|
+
if self.dest:
|
|
159
|
+
stage_path /= (
|
|
160
|
+
PurePosixPath(self.dest).parent if self._is_dest_a_file() else self.dest
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
stage_path /= (
|
|
164
|
+
self._path_until_asterisk()
|
|
165
|
+
if glob.has_magic(str(self.path))
|
|
166
|
+
else PurePosixPath(self.path).parent
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return str(stage_path) + "/"
|
|
170
|
+
|
|
171
|
+
def import_path(self, stage: FQN | str | None) -> str:
|
|
172
|
+
"""Path for UDF/sproc imports clause."""
|
|
173
|
+
return self.upload_path(stage) + self._artifact_name
|
|
174
|
+
|
|
175
|
+
def _is_dest_a_file(self) -> bool:
|
|
176
|
+
if not self.dest:
|
|
177
|
+
return False
|
|
178
|
+
return re.search(r"\.[a-zA-Z0-9]{2,4}$", self.dest) is not None
|
|
179
|
+
|
|
180
|
+
def _path_until_asterisk(self) -> Path:
|
|
181
|
+
path = []
|
|
182
|
+
for part in self.path.parts:
|
|
183
|
+
if glob.has_magic(part):
|
|
184
|
+
break
|
|
185
|
+
else:
|
|
186
|
+
path.append(part)
|
|
187
|
+
return Path(*path[:-1])
|
|
188
|
+
|
|
189
|
+
# Can be removed after removing ENABLE_SNOWPARK_GLOB_SUPPORT feature flag.
|
|
190
|
+
def build(self) -> None:
|
|
191
|
+
raise NotImplementedError("Not implemented in Artifact class.")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(unsafe_hash=True)
|
|
195
|
+
class ArtifactOldBuild(Artifact):
|
|
196
|
+
"""Helper for getting paths related to given artifact."""
|
|
197
|
+
|
|
198
|
+
path: Path
|
|
199
|
+
dest: str | None = None
|
|
200
|
+
|
|
201
|
+
def __init__(self, path: Path, dest: Optional[str] = None) -> None:
|
|
202
|
+
super().__init__(project_root=Path(), bundle_root=Path(), path=path, dest=dest)
|
|
203
|
+
|
|
71
204
|
@property
|
|
72
|
-
def
|
|
205
|
+
def _artifact_name(self) -> str:
|
|
73
206
|
if self.path.is_dir():
|
|
74
207
|
return self.path.stem + ".zip"
|
|
75
208
|
return self.path.name
|
|
@@ -77,13 +210,13 @@ class Artefact:
|
|
|
77
210
|
@property
|
|
78
211
|
def post_build_path(self) -> Path:
|
|
79
212
|
"""
|
|
80
|
-
Returns post-build
|
|
213
|
+
Returns post-build artifact path. Directories are mapped to corresponding .zip files.
|
|
81
214
|
"""
|
|
82
|
-
return self.path.parent / self.
|
|
215
|
+
return self.path.parent / self._artifact_name
|
|
83
216
|
|
|
84
217
|
def upload_path(self, stage: FQN | str | None) -> str:
|
|
85
218
|
"""
|
|
86
|
-
Path on stage to which the
|
|
219
|
+
Path on stage to which the artifact should be uploaded.
|
|
87
220
|
"""
|
|
88
221
|
stage = stage or DEPLOYMENT_STAGE
|
|
89
222
|
if isinstance(stage, str):
|
|
@@ -96,10 +229,10 @@ class Artefact:
|
|
|
96
229
|
|
|
97
230
|
def import_path(self, stage: FQN | str | None) -> str:
|
|
98
231
|
"""Path for UDF/sproc imports clause."""
|
|
99
|
-
return self.upload_path(stage) + self.
|
|
232
|
+
return self.upload_path(stage) + self._artifact_name
|
|
100
233
|
|
|
101
234
|
def build(self) -> None:
|
|
102
|
-
"""Build the
|
|
235
|
+
"""Build the artifact. Applies only to directories. Files are untouched."""
|
|
103
236
|
if not self.path.is_dir():
|
|
104
237
|
return
|
|
105
238
|
cli_console.step(f"Creating: {self.post_build_path.name}")
|
|
@@ -20,6 +20,10 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Dict, List, Literal
|
|
21
21
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
22
22
|
|
|
23
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
24
|
+
from snowflake.cli.api.console import cli_console
|
|
25
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
26
|
+
|
|
23
27
|
log = logging.getLogger(__name__)
|
|
24
28
|
|
|
25
29
|
IGNORED_FILES = [
|
|
@@ -64,7 +68,10 @@ def zip_dir(
|
|
|
64
68
|
mode: Literal["r", "w", "x", "a"] = "w",
|
|
65
69
|
) -> None:
|
|
66
70
|
|
|
67
|
-
if
|
|
71
|
+
if not dest_zip.parent.exists():
|
|
72
|
+
SecurePath(dest_zip).parent.mkdir(parents=True)
|
|
73
|
+
|
|
74
|
+
if isinstance(source, Path) or isinstance(source, SecurePath):
|
|
68
75
|
source = [source]
|
|
69
76
|
|
|
70
77
|
files_to_pack: Dict[Path, List[Path]] = {
|
|
@@ -74,11 +81,36 @@ def zip_dir(
|
|
|
74
81
|
|
|
75
82
|
with ZipFile(dest_zip, mode, ZIP_DEFLATED, allowZip64=True) as package_zip:
|
|
76
83
|
for src, files in files_to_pack.items():
|
|
84
|
+
if isinstance(src, SecurePath):
|
|
85
|
+
src = src.path
|
|
77
86
|
for file in files:
|
|
78
87
|
log.debug("Adding %s to %s", file, dest_zip)
|
|
79
88
|
package_zip.write(file, arcname=file.relative_to(src))
|
|
80
89
|
|
|
81
90
|
|
|
91
|
+
def zip_dir_using_bundle_map(
|
|
92
|
+
bundle_map: BundleMap,
|
|
93
|
+
dest_zip: Path,
|
|
94
|
+
mode: Literal["r", "w", "x", "a"] = "w",
|
|
95
|
+
) -> None:
|
|
96
|
+
if not dest_zip.parent.exists():
|
|
97
|
+
SecurePath(dest_zip).parent.mkdir(parents=True)
|
|
98
|
+
|
|
99
|
+
with ZipFile(dest_zip, mode, ZIP_DEFLATED, allowZip64=True) as package_zip:
|
|
100
|
+
cli_console.step(f"Creating: {dest_zip}")
|
|
101
|
+
for src, _ in bundle_map.all_mappings(expand_directories=True):
|
|
102
|
+
if src.is_file():
|
|
103
|
+
log.debug("Adding %s to %s", src, dest_zip)
|
|
104
|
+
package_zip.write(src, arcname=_path_without_top_level_directory(src))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _path_without_top_level_directory(path: Path) -> str:
|
|
108
|
+
path_parts = path.parts
|
|
109
|
+
if len(path_parts) > 1:
|
|
110
|
+
return str(Path(*path_parts[1:]))
|
|
111
|
+
return str(path)
|
|
112
|
+
|
|
113
|
+
|
|
82
114
|
def _to_be_zipped(file: Path) -> bool:
|
|
83
115
|
for pattern in IGNORED_FILES:
|
|
84
116
|
# This has to be a string because of fnmatch
|
|
@@ -14,28 +14,37 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from typing import Optional
|
|
17
|
+
from typing import List, Optional
|
|
18
18
|
|
|
19
19
|
import typer
|
|
20
20
|
from click import ClickException
|
|
21
21
|
from snowflake.cli._plugins.object.command_aliases import (
|
|
22
22
|
add_object_command_aliases,
|
|
23
23
|
)
|
|
24
|
-
from snowflake.cli._plugins.object.common import CommentOption
|
|
25
|
-
from snowflake.cli._plugins.spcs.common import
|
|
26
|
-
|
|
24
|
+
from snowflake.cli._plugins.object.common import CommentOption, Tag, TagOption
|
|
25
|
+
from snowflake.cli._plugins.spcs.common import validate_and_set_instances
|
|
26
|
+
from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity_model import (
|
|
27
|
+
ComputePoolEntityModel,
|
|
27
28
|
)
|
|
28
29
|
from snowflake.cli._plugins.spcs.compute_pool.manager import ComputePoolManager
|
|
30
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
29
31
|
from snowflake.cli.api.commands.flags import (
|
|
30
32
|
IfNotExistsOption,
|
|
31
33
|
OverrideableOption,
|
|
34
|
+
entity_argument,
|
|
32
35
|
identifier_argument,
|
|
33
36
|
like_option,
|
|
34
37
|
)
|
|
35
38
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
36
39
|
from snowflake.cli.api.constants import ObjectType
|
|
37
40
|
from snowflake.cli.api.identifiers import FQN
|
|
38
|
-
from snowflake.cli.api.output.types import
|
|
41
|
+
from snowflake.cli.api.output.types import (
|
|
42
|
+
CommandResult,
|
|
43
|
+
SingleQueryResult,
|
|
44
|
+
)
|
|
45
|
+
from snowflake.cli.api.project.definition_helper import (
|
|
46
|
+
get_entity_from_project_definition,
|
|
47
|
+
)
|
|
39
48
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
40
49
|
|
|
41
50
|
app = SnowTyperFactory(
|
|
@@ -123,6 +132,7 @@ def create(
|
|
|
123
132
|
help="Starts the compute pool in a suspended state.",
|
|
124
133
|
),
|
|
125
134
|
auto_suspend_secs: int = AutoSuspendSecsOption(),
|
|
135
|
+
tags: Optional[List[Tag]] = TagOption(help="Tag for the compute pool."),
|
|
126
136
|
comment: Optional[str] = CommentOption(help=_COMMENT_HELP),
|
|
127
137
|
if_not_exists: bool = IfNotExistsOption(),
|
|
128
138
|
**options,
|
|
@@ -139,12 +149,50 @@ def create(
|
|
|
139
149
|
auto_resume=auto_resume,
|
|
140
150
|
initially_suspended=initially_suspended,
|
|
141
151
|
auto_suspend_secs=auto_suspend_secs,
|
|
152
|
+
tags=tags,
|
|
142
153
|
comment=comment,
|
|
143
154
|
if_not_exists=if_not_exists,
|
|
144
155
|
)
|
|
145
156
|
return SingleQueryResult(cursor)
|
|
146
157
|
|
|
147
158
|
|
|
159
|
+
@app.command("deploy", requires_connection=True)
|
|
160
|
+
@with_project_definition()
|
|
161
|
+
def deploy(
|
|
162
|
+
entity_id: str = entity_argument("compute-pool"),
|
|
163
|
+
upgrade: bool = typer.Option(
|
|
164
|
+
False,
|
|
165
|
+
"--upgrade",
|
|
166
|
+
help="Updates the existing compute pool. Can update min_nodes, max_nodes, auto_resume, auto_suspend_seconds and comment.",
|
|
167
|
+
),
|
|
168
|
+
**options,
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Deploys a compute pool from the project definition file.
|
|
172
|
+
"""
|
|
173
|
+
compute_pool: ComputePoolEntityModel = get_entity_from_project_definition(
|
|
174
|
+
entity_type=ObjectType.COMPUTE_POOL, entity_id=entity_id
|
|
175
|
+
)
|
|
176
|
+
max_nodes = validate_and_set_instances(
|
|
177
|
+
compute_pool.min_nodes, compute_pool.max_nodes, "nodes"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
cursor = ComputePoolManager().deploy(
|
|
181
|
+
pool_name=compute_pool.fqn.identifier,
|
|
182
|
+
min_nodes=compute_pool.min_nodes,
|
|
183
|
+
max_nodes=max_nodes,
|
|
184
|
+
instance_family=compute_pool.instance_family,
|
|
185
|
+
auto_resume=compute_pool.auto_resume,
|
|
186
|
+
initially_suspended=compute_pool.initially_suspended,
|
|
187
|
+
auto_suspend_seconds=compute_pool.auto_suspend_seconds,
|
|
188
|
+
tags=compute_pool.tags,
|
|
189
|
+
comment=compute_pool.comment,
|
|
190
|
+
upgrade=upgrade,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return SingleQueryResult(cursor)
|
|
194
|
+
|
|
195
|
+
|
|
148
196
|
@app.command("stop-all", requires_connection=True)
|
|
149
197
|
def stop_all(name: FQN = ComputePoolNameArgument, **options) -> CommandResult:
|
|
150
198
|
"""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_validator
|
|
4
|
+
from snowflake.cli._plugins.object.common import Tag
|
|
5
|
+
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
6
|
+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
|
|
7
|
+
from snowflake.cli.api.project.util import to_string_literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComputePoolEntityModel(EntityModelBase):
|
|
11
|
+
type: Literal["compute-pool"] = DiscriminatorField() # noqa: A003
|
|
12
|
+
min_nodes: Optional[int] = Field(title="Minimum number of nodes", default=1, ge=1)
|
|
13
|
+
max_nodes: Optional[int] = Field(
|
|
14
|
+
title="Maximum number of nodes", default=None, ge=1
|
|
15
|
+
)
|
|
16
|
+
instance_family: str = Field(title="Name of the instance family", default=None)
|
|
17
|
+
auto_resume: Optional[bool] = Field(
|
|
18
|
+
title="The compute pool will automatically resume when a service or job is submitted to it",
|
|
19
|
+
default=True,
|
|
20
|
+
)
|
|
21
|
+
initially_suspended: Optional[bool] = Field(
|
|
22
|
+
title="Starts the compute pool in a suspended state", default=False
|
|
23
|
+
)
|
|
24
|
+
auto_suspend_seconds: Optional[int] = Field(
|
|
25
|
+
title="Number of seconds of inactivity after which you want Snowflake to automatically suspend the compute pool",
|
|
26
|
+
default=3600,
|
|
27
|
+
ge=1,
|
|
28
|
+
)
|
|
29
|
+
comment: Optional[str] = Field(title="Comment for the compute pool", default=None)
|
|
30
|
+
tags: Optional[List[Tag]] = Field(title="Tag for the compute pool", default=None)
|
|
31
|
+
|
|
32
|
+
@field_validator("comment")
|
|
33
|
+
@classmethod
|
|
34
|
+
def _convert_artifacts(cls, comment: Optional[str]):
|
|
35
|
+
if comment:
|
|
36
|
+
return to_string_literal(comment)
|
|
37
|
+
return comment
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from typing import List, Optional
|
|
18
18
|
|
|
19
|
+
from snowflake.cli._plugins.object.common import Tag
|
|
19
20
|
from snowflake.cli._plugins.spcs.common import (
|
|
20
21
|
NoPropertiesProvidedError,
|
|
21
22
|
handle_object_already_exists,
|
|
@@ -37,9 +38,11 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
37
38
|
auto_resume: bool,
|
|
38
39
|
initially_suspended: bool,
|
|
39
40
|
auto_suspend_secs: int,
|
|
41
|
+
tags: Optional[List[Tag]],
|
|
40
42
|
comment: Optional[str],
|
|
41
43
|
if_not_exists: bool,
|
|
42
44
|
) -> SnowflakeCursor:
|
|
45
|
+
|
|
43
46
|
create_statement = "CREATE COMPUTE POOL"
|
|
44
47
|
if if_not_exists:
|
|
45
48
|
create_statement = f"{create_statement} IF NOT EXISTS"
|
|
@@ -55,11 +58,52 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
55
58
|
if comment:
|
|
56
59
|
query.append(f"COMMENT = {comment}")
|
|
57
60
|
|
|
61
|
+
if tags:
|
|
62
|
+
tag_list = ",".join(f"{t.name}={t.value_string_literal()}" for t in tags)
|
|
63
|
+
query.append(f"WITH TAG ({tag_list})")
|
|
64
|
+
|
|
58
65
|
try:
|
|
59
66
|
return self.execute_query(strip_empty_lines(query))
|
|
60
67
|
except ProgrammingError as e:
|
|
61
68
|
handle_object_already_exists(e, ObjectType.COMPUTE_POOL, pool_name)
|
|
62
69
|
|
|
70
|
+
def deploy(
|
|
71
|
+
self,
|
|
72
|
+
pool_name: str,
|
|
73
|
+
min_nodes: int,
|
|
74
|
+
max_nodes: int,
|
|
75
|
+
instance_family: str,
|
|
76
|
+
auto_resume: bool,
|
|
77
|
+
initially_suspended: bool,
|
|
78
|
+
auto_suspend_seconds: int,
|
|
79
|
+
tags: Optional[List[Tag]],
|
|
80
|
+
comment: Optional[str],
|
|
81
|
+
upgrade: bool,
|
|
82
|
+
):
|
|
83
|
+
|
|
84
|
+
if upgrade:
|
|
85
|
+
return self.set_property(
|
|
86
|
+
pool_name=pool_name,
|
|
87
|
+
min_nodes=min_nodes,
|
|
88
|
+
max_nodes=max_nodes,
|
|
89
|
+
auto_resume=auto_resume,
|
|
90
|
+
auto_suspend_secs=auto_suspend_seconds,
|
|
91
|
+
comment=comment,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
return self.create(
|
|
95
|
+
pool_name=pool_name,
|
|
96
|
+
min_nodes=min_nodes,
|
|
97
|
+
max_nodes=max_nodes,
|
|
98
|
+
instance_family=instance_family,
|
|
99
|
+
auto_resume=auto_resume,
|
|
100
|
+
initially_suspended=initially_suspended,
|
|
101
|
+
auto_suspend_secs=auto_suspend_seconds,
|
|
102
|
+
tags=tags,
|
|
103
|
+
comment=comment,
|
|
104
|
+
if_not_exists=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
63
107
|
def stop(self, pool_name: str) -> SnowflakeCursor:
|
|
64
108
|
return self.execute_query(f"alter compute pool {pool_name} stop all")
|
|
65
109
|
|
|
@@ -95,6 +139,7 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
95
139
|
for property_name, value in property_pairs:
|
|
96
140
|
if value is not None:
|
|
97
141
|
query.append(f"{property_name} = {value}")
|
|
142
|
+
|
|
98
143
|
return self.execute_query(strip_empty_lines(query))
|
|
99
144
|
|
|
100
145
|
def unset_property(
|
|
@@ -26,9 +26,11 @@ from snowflake.cli._plugins.object.command_aliases import (
|
|
|
26
26
|
)
|
|
27
27
|
from snowflake.cli._plugins.spcs.image_registry.manager import RegistryManager
|
|
28
28
|
from snowflake.cli._plugins.spcs.image_repository.manager import ImageRepositoryManager
|
|
29
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
29
30
|
from snowflake.cli.api.commands.flags import (
|
|
30
31
|
IfNotExistsOption,
|
|
31
32
|
ReplaceOption,
|
|
33
|
+
entity_argument,
|
|
32
34
|
identifier_argument,
|
|
33
35
|
like_option,
|
|
34
36
|
)
|
|
@@ -42,6 +44,9 @@ from snowflake.cli.api.output.types import (
|
|
|
42
44
|
QueryResult,
|
|
43
45
|
SingleQueryResult,
|
|
44
46
|
)
|
|
47
|
+
from snowflake.cli.api.project.definition_helper import (
|
|
48
|
+
get_entity_from_project_definition,
|
|
49
|
+
)
|
|
45
50
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
46
51
|
|
|
47
52
|
app = SnowTyperFactory(
|
|
@@ -94,6 +99,30 @@ def create(
|
|
|
94
99
|
)
|
|
95
100
|
|
|
96
101
|
|
|
102
|
+
@app.command(requires_connection=True)
|
|
103
|
+
@with_project_definition()
|
|
104
|
+
def deploy(
|
|
105
|
+
entity_id: str = entity_argument("image-repository"),
|
|
106
|
+
replace: bool = ReplaceOption(
|
|
107
|
+
help="Replace the image repository if it already exists."
|
|
108
|
+
),
|
|
109
|
+
**options,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Deploys a new image repository from snowflake.yml file.
|
|
113
|
+
"""
|
|
114
|
+
image_repository = get_entity_from_project_definition(
|
|
115
|
+
ObjectType.IMAGE_REPOSITORY, entity_id
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
cursor = ImageRepositoryManager().create(
|
|
119
|
+
name=image_repository.fqn.identifier,
|
|
120
|
+
if_not_exists=False,
|
|
121
|
+
replace=replace,
|
|
122
|
+
)
|
|
123
|
+
return SingleQueryResult(cursor)
|
|
124
|
+
|
|
125
|
+
|
|
97
126
|
@app.command("list-images", requires_connection=True)
|
|
98
127
|
def list_images(
|
|
99
128
|
name: FQN = REPO_NAME_ARGUMENT,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
4
|
+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ImageRepositoryEntityModel(EntityModelBase):
|
|
8
|
+
type: Literal["image-repository"] = DiscriminatorField() # noqa: A003
|
|
@@ -64,7 +64,7 @@ class ImageRepositoryManager(SqlExecutionMixin):
|
|
|
64
64
|
name: str,
|
|
65
65
|
if_not_exists: bool,
|
|
66
66
|
replace: bool,
|
|
67
|
-
):
|
|
67
|
+
) -> SnowflakeCursor:
|
|
68
68
|
if if_not_exists and replace:
|
|
69
69
|
raise ValueError(
|
|
70
70
|
"'replace' and 'if_not_exists' options are mutually exclusive for ImageRepositoryManager.create"
|
|
@@ -29,9 +29,16 @@ from snowflake.cli._plugins.spcs.common import (
|
|
|
29
29
|
validate_and_set_instances,
|
|
30
30
|
)
|
|
31
31
|
from snowflake.cli._plugins.spcs.services.manager import ServiceManager
|
|
32
|
+
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
|
|
33
|
+
from snowflake.cli._plugins.spcs.services.service_project_paths import (
|
|
34
|
+
ServiceProjectPaths,
|
|
35
|
+
)
|
|
36
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
37
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
32
38
|
from snowflake.cli.api.commands.flags import (
|
|
33
39
|
IfNotExistsOption,
|
|
34
40
|
OverrideableOption,
|
|
41
|
+
entity_argument,
|
|
35
42
|
identifier_argument,
|
|
36
43
|
like_option,
|
|
37
44
|
)
|
|
@@ -51,6 +58,9 @@ from snowflake.cli.api.output.types import (
|
|
|
51
58
|
SingleQueryResult,
|
|
52
59
|
StreamResult,
|
|
53
60
|
)
|
|
61
|
+
from snowflake.cli.api.project.definition_helper import (
|
|
62
|
+
get_entity_from_project_definition,
|
|
63
|
+
)
|
|
54
64
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
55
65
|
|
|
56
66
|
app = SnowTyperFactory(
|
|
@@ -200,6 +210,47 @@ def create(
|
|
|
200
210
|
return SingleQueryResult(cursor)
|
|
201
211
|
|
|
202
212
|
|
|
213
|
+
@app.command(requires_connection=True)
|
|
214
|
+
@with_project_definition()
|
|
215
|
+
def deploy(
|
|
216
|
+
entity_id: str = entity_argument("service"),
|
|
217
|
+
upgrade: bool = typer.Option(
|
|
218
|
+
False,
|
|
219
|
+
"--upgrade",
|
|
220
|
+
help="Updates the existing service. Can update min_instances, max_instances, query_warehouse, auto_resume, external_access_integrations and comment.",
|
|
221
|
+
),
|
|
222
|
+
**options,
|
|
223
|
+
) -> CommandResult:
|
|
224
|
+
"""
|
|
225
|
+
Deploys a service defined in the project definition file.
|
|
226
|
+
"""
|
|
227
|
+
service: ServiceEntityModel = get_entity_from_project_definition(
|
|
228
|
+
entity_type=ObjectType.SERVICE,
|
|
229
|
+
entity_id=entity_id,
|
|
230
|
+
)
|
|
231
|
+
service_project_paths = ServiceProjectPaths(get_cli_context().project_root)
|
|
232
|
+
max_instances = validate_and_set_instances(
|
|
233
|
+
service.min_instances, service.max_instances, "instances"
|
|
234
|
+
)
|
|
235
|
+
cursor = ServiceManager().deploy(
|
|
236
|
+
service_name=service.fqn.identifier,
|
|
237
|
+
stage=service.stage,
|
|
238
|
+
artifacts=service.artifacts,
|
|
239
|
+
compute_pool=service.compute_pool,
|
|
240
|
+
spec_path=service.spec_file,
|
|
241
|
+
min_instances=service.min_instances,
|
|
242
|
+
max_instances=max_instances,
|
|
243
|
+
auto_resume=service.auto_resume,
|
|
244
|
+
external_access_integrations=service.external_access_integrations,
|
|
245
|
+
query_warehouse=service.query_warehouse,
|
|
246
|
+
tags=service.tags,
|
|
247
|
+
comment=service.comment,
|
|
248
|
+
service_project_paths=service_project_paths,
|
|
249
|
+
upgrade=upgrade,
|
|
250
|
+
)
|
|
251
|
+
return SingleQueryResult(cursor)
|
|
252
|
+
|
|
253
|
+
|
|
203
254
|
@app.command(requires_connection=True)
|
|
204
255
|
def execute_job(
|
|
205
256
|
name: FQN = ServiceNameArgument,
|
|
@@ -369,7 +420,6 @@ def events(
|
|
|
369
420
|
|
|
370
421
|
@app.command(
|
|
371
422
|
requires_connection=True,
|
|
372
|
-
is_enabled=FeatureFlag.ENABLE_SPCS_SERVICE_METRICS.is_enabled,
|
|
373
423
|
)
|
|
374
424
|
def metrics(
|
|
375
425
|
name: FQN = ServiceNameArgument,
|