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.
Files changed (112) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +220 -197
  4. snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
  5. snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
  6. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +4 -30
  7. snowflake/cli/_app/printing.py +2 -2
  8. snowflake/cli/_plugins/connection/commands.py +2 -4
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +3 -4
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
  19. snowflake/cli/_plugins/nativeapp/commands.py +21 -19
  20. snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
  21. snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
  22. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
  23. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
  24. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
  25. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
  26. snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
  27. snowflake/cli/_plugins/notebook/commands.py +54 -2
  28. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  29. snowflake/cli/_plugins/notebook/manager.py +3 -3
  30. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  31. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  32. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  33. snowflake/cli/_plugins/notebook/types.py +3 -0
  34. snowflake/cli/_plugins/plugin/commands.py +79 -0
  35. snowflake/cli/_plugins/plugin/manager.py +74 -0
  36. snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
  37. snowflake/cli/_plugins/project/__init__.py +0 -0
  38. snowflake/cli/_plugins/project/commands.py +157 -0
  39. snowflake/cli/_plugins/project/feature_flags.py +22 -0
  40. snowflake/cli/_plugins/project/manager.py +76 -0
  41. snowflake/cli/_plugins/project/plugin_spec.py +30 -0
  42. snowflake/cli/_plugins/project/project_entity_model.py +40 -0
  43. snowflake/cli/_plugins/snowpark/commands.py +49 -30
  44. snowflake/cli/_plugins/snowpark/common.py +47 -2
  45. snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
  46. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  47. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  48. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  49. snowflake/cli/_plugins/spcs/compute_pool/commands.py +53 -5
  50. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
  51. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
  52. snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
  53. snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
  54. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
  55. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
  56. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  57. snowflake/cli/_plugins/spcs/services/commands.py +51 -1
  58. snowflake/cli/_plugins/spcs/services/manager.py +114 -0
  59. snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
  60. snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
  61. snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
  62. snowflake/cli/_plugins/stage/commands.py +2 -1
  63. snowflake/cli/_plugins/stage/diff.py +60 -39
  64. snowflake/cli/_plugins/stage/manager.py +26 -13
  65. snowflake/cli/_plugins/stage/utils.py +1 -1
  66. snowflake/cli/_plugins/streamlit/commands.py +18 -24
  67. snowflake/cli/_plugins/streamlit/manager.py +37 -27
  68. snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
  69. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  70. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  71. snowflake/cli/_plugins/workspace/commands.py +3 -3
  72. snowflake/cli/_plugins/workspace/manager.py +1 -1
  73. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  74. snowflake/cli/api/artifacts/common.py +78 -0
  75. snowflake/cli/api/artifacts/upload.py +51 -0
  76. snowflake/cli/api/artifacts/utils.py +82 -0
  77. snowflake/cli/api/cli_global_context.py +14 -1
  78. snowflake/cli/api/commands/flags.py +34 -13
  79. snowflake/cli/api/commands/snow_typer.py +12 -0
  80. snowflake/cli/api/commands/utils.py +30 -2
  81. snowflake/cli/api/config.py +15 -10
  82. snowflake/cli/api/constants.py +1 -0
  83. snowflake/cli/api/entities/common.py +14 -32
  84. snowflake/cli/api/entities/resolver.py +160 -0
  85. snowflake/cli/api/entities/utils.py +56 -15
  86. snowflake/cli/api/errno.py +3 -0
  87. snowflake/cli/api/exceptions.py +8 -1
  88. snowflake/cli/api/feature_flags.py +1 -1
  89. snowflake/cli/api/plugins/plugin_config.py +43 -4
  90. snowflake/cli/api/project/definition_conversion.py +3 -2
  91. snowflake/cli/api/project/definition_helper.py +31 -0
  92. snowflake/cli/api/project/project_paths.py +28 -0
  93. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  94. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  95. snowflake/cli/api/project/schemas/project_definition.py +27 -0
  96. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  97. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  98. snowflake/cli/api/secure_path.py +6 -0
  99. snowflake/cli/api/sql_execution.py +5 -1
  100. snowflake/cli/api/stage_path.py +7 -2
  101. snowflake/cli/api/utils/graph.py +3 -0
  102. snowflake/cli/api/utils/path_utils.py +24 -0
  103. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +12 -13
  104. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +109 -85
  105. snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
  106. snowflake/cli/api/__init__.py +0 -48
  107. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  108. /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
  109. /snowflake/cli/{_app/api_impl/plugin → api/artifacts}/__init__.py +0 -0
  110. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
  111. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
  112. {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 represents allows you to manage files paths related to given project.
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 get_artefact_dto(self, artifact_path: PathMapping) -> Artefact:
41
- return Artefact(
42
- dest=artifact_path.dest,
43
- path=self.path_relative_to_root(artifact_path.src),
44
- )
45
-
46
- def get_dependencies_artefact(self) -> Artefact:
47
- return Artefact(dest=None, path=self.dependencies)
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 dependencies(self) -> Path:
61
- return self.path_relative_to_root(Path("dependencies.zip"))
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 Artefact:
66
- """Helper for getting paths related to given artefact."""
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 _artefact_name(self) -> str:
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 artefact path. Directories are mapped to corresponding .zip files.
213
+ Returns post-build artifact path. Directories are mapped to corresponding .zip files.
81
214
  """
82
- return self.path.parent / self._artefact_name
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 artefact should be uploaded.
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._artefact_name
232
+ return self.upload_path(stage) + self._artifact_name
100
233
 
101
234
  def build(self) -> None:
102
- """Build the artefact. Applies only to directories. Files are untouched."""
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 isinstance(source, Path):
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
- validate_and_set_instances,
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 CommandResult, SingleQueryResult
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,8 @@
1
+ from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity_model import (
2
+ ComputePoolEntityModel,
3
+ )
4
+ from snowflake.cli.api.entities.common import EntityBase
5
+
6
+
7
+ class ComputePoolEntity(EntityBase[ComputePoolEntityModel]):
8
+ pass
@@ -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 snowflake.cli._plugins.spcs.image_repository.image_repository_entity_model import (
2
+ ImageRepositoryEntityModel,
3
+ )
4
+ from snowflake.cli.api.entities.common import EntityBase
5
+
6
+
7
+ class ImageRepositoryEntity(EntityBase[ImageRepositoryEntityModel]):
8
+ pass
@@ -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,