snowflake-cli 3.4.1__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 (53) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +1 -10
  3. snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
  4. snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
  5. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
  6. snowflake/cli/_app/printing.py +2 -2
  7. snowflake/cli/_plugins/connection/commands.py +2 -4
  8. snowflake/cli/_plugins/helpers/commands.py +3 -4
  9. snowflake/cli/_plugins/notebook/commands.py +3 -4
  10. snowflake/cli/_plugins/plugin/commands.py +79 -0
  11. snowflake/cli/_plugins/plugin/manager.py +74 -0
  12. snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
  13. snowflake/cli/_plugins/project/__init__.py +0 -0
  14. snowflake/cli/_plugins/project/commands.py +157 -0
  15. snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -0
  16. snowflake/cli/_plugins/project/manager.py +76 -0
  17. snowflake/cli/_plugins/project/plugin_spec.py +30 -0
  18. snowflake/cli/_plugins/project/project_entity_model.py +40 -0
  19. snowflake/cli/_plugins/snowpark/commands.py +2 -1
  20. snowflake/cli/_plugins/spcs/compute_pool/commands.py +53 -5
  21. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
  22. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
  23. snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
  24. snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
  25. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
  26. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
  27. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  28. snowflake/cli/_plugins/spcs/services/commands.py +53 -0
  29. snowflake/cli/_plugins/spcs/services/manager.py +114 -0
  30. snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
  31. snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
  32. snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
  33. snowflake/cli/_plugins/stage/manager.py +2 -2
  34. snowflake/cli/_plugins/streamlit/commands.py +9 -24
  35. snowflake/cli/_plugins/streamlit/manager.py +5 -36
  36. snowflake/cli/api/artifacts/upload.py +51 -0
  37. snowflake/cli/api/commands/flags.py +24 -9
  38. snowflake/cli/api/commands/snow_typer.py +12 -0
  39. snowflake/cli/api/commands/utils.py +2 -0
  40. snowflake/cli/api/config.py +15 -10
  41. snowflake/cli/api/exceptions.py +8 -1
  42. snowflake/cli/api/feature_flags.py +1 -0
  43. snowflake/cli/api/plugins/plugin_config.py +43 -4
  44. snowflake/cli/api/project/definition_helper.py +31 -0
  45. snowflake/cli/api/project/schemas/entities/entities.py +26 -0
  46. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +9 -9
  47. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +51 -36
  48. snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
  49. snowflake/cli/api/__init__.py +0 -48
  50. /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
  51. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
  52. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
  53. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,40 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import annotations
15
+
16
+ from typing import Literal, Optional, TypeVar
17
+
18
+ from pydantic import Field
19
+ from snowflake.cli.api.entities.common import EntityBase, attach_spans_to_entity_actions
20
+ from snowflake.cli.api.project.schemas.entities.common import (
21
+ EntityModelBaseWithArtifacts,
22
+ )
23
+ from snowflake.cli.api.project.schemas.updatable_model import (
24
+ DiscriminatorField,
25
+ )
26
+
27
+ T = TypeVar("T")
28
+
29
+
30
+ class ProjectEntityModel(EntityModelBaseWithArtifacts):
31
+ type: Literal["project"] = DiscriminatorField() # noqa: A003
32
+ stage: Optional[str] = Field(
33
+ title="Stage in which the project artifacts will be stored", default=None
34
+ )
35
+ main_file: Optional[str] = Field(title="Path to the main file of the project")
36
+
37
+
38
+ @attach_spans_to_entity_actions(entity_name="project")
39
+ class ProjectEntity(EntityBase[ProjectEntityModel]):
40
+ """Placeholder for project entity"""
@@ -129,7 +129,8 @@ LikeOption = like_option(
129
129
  @with_project_definition()
130
130
  def deploy(
131
131
  replace: bool = ReplaceOption(
132
- help="Replaces procedure or function if there were changes in the definition."
132
+ help="Replaces procedure or function if there were changes in the definition. It only uploads new and "
133
+ "overwrites existing files, but does not remove any files already on the stage."
133
134
  ),
134
135
  force_replace: bool = ForceReplaceOption(),
135
136
  **options,
@@ -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
  )
@@ -40,6 +47,7 @@ from snowflake.cli.api.constants import ObjectType
40
47
  from snowflake.cli.api.exceptions import (
41
48
  IncompatibleParametersError,
42
49
  )
50
+ from snowflake.cli.api.feature_flags import FeatureFlag
43
51
  from snowflake.cli.api.identifiers import FQN
44
52
  from snowflake.cli.api.output.types import (
45
53
  CollectionResult,
@@ -50,6 +58,9 @@ from snowflake.cli.api.output.types import (
50
58
  SingleQueryResult,
51
59
  StreamResult,
52
60
  )
61
+ from snowflake.cli.api.project.definition_helper import (
62
+ get_entity_from_project_definition,
63
+ )
53
64
  from snowflake.cli.api.project.util import is_valid_object_name
54
65
 
55
66
  app = SnowTyperFactory(
@@ -199,6 +210,47 @@ def create(
199
210
  return SingleQueryResult(cursor)
200
211
 
201
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
+
202
254
  @app.command(requires_connection=True)
203
255
  def execute_job(
204
256
  name: FQN = ServiceNameArgument,
@@ -320,6 +372,7 @@ def logs(
320
372
 
321
373
  @app.command(
322
374
  requires_connection=True,
375
+ is_enabled=FeatureFlag.ENABLE_SPCS_SERVICE_EVENTS.is_enabled,
323
376
  )
324
377
  def events(
325
378
  name: FQN = ServiceNameArgument,
@@ -35,9 +35,17 @@ from snowflake.cli._plugins.spcs.common import (
35
35
  new_logs_only,
36
36
  strip_empty_lines,
37
37
  )
38
+ from snowflake.cli._plugins.spcs.services.service_project_paths import (
39
+ ServiceProjectPaths,
40
+ )
41
+ from snowflake.cli._plugins.stage.manager import StageManager
42
+ from snowflake.cli.api.artifacts.utils import bundle_artifacts
38
43
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
44
+ from snowflake.cli.api.identifiers import FQN
45
+ from snowflake.cli.api.project.schemas.entities.common import Artifacts
39
46
  from snowflake.cli.api.secure_path import SecurePath
40
47
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
48
+ from snowflake.cli.api.stage_path import StagePath
41
49
  from snowflake.connector.cursor import DictCursor, SnowflakeCursor
42
50
  from snowflake.connector.errors import ProgrammingError
43
51
 
@@ -95,6 +103,112 @@ class ServiceManager(SqlExecutionMixin):
95
103
  except ProgrammingError as e:
96
104
  handle_object_already_exists(e, ObjectType.SERVICE, service_name)
97
105
 
106
+ def deploy(
107
+ self,
108
+ service_name: str,
109
+ stage: str,
110
+ artifacts: List[str],
111
+ compute_pool: str,
112
+ spec_path: Path,
113
+ min_instances: int,
114
+ max_instances: int,
115
+ auto_resume: bool,
116
+ external_access_integrations: Optional[List[str]],
117
+ query_warehouse: Optional[str],
118
+ tags: Optional[List[Tag]],
119
+ comment: Optional[str],
120
+ service_project_paths: ServiceProjectPaths,
121
+ upgrade: bool,
122
+ ) -> SnowflakeCursor:
123
+ stage_manager = StageManager()
124
+ stage_manager.create(fqn=FQN.from_stage(stage))
125
+
126
+ stage = stage_manager.get_standard_stage_prefix(stage)
127
+ self._upload_artifacts(
128
+ stage_manager=stage_manager,
129
+ service_project_paths=service_project_paths,
130
+ artifacts=artifacts,
131
+ stage=stage,
132
+ )
133
+
134
+ if upgrade:
135
+ self.set_property(
136
+ service_name=service_name,
137
+ min_instances=min_instances,
138
+ max_instances=max_instances,
139
+ query_warehouse=query_warehouse,
140
+ auto_resume=auto_resume,
141
+ external_access_integrations=external_access_integrations,
142
+ comment=comment,
143
+ )
144
+ query = [
145
+ f"ALTER SERVICE {service_name}",
146
+ f"FROM {stage}",
147
+ f"SPECIFICATION_FILE = '{spec_path}'",
148
+ ]
149
+ return self.execute_query(strip_empty_lines(query))
150
+ else:
151
+ query = [
152
+ f"CREATE SERVICE {service_name}",
153
+ f"IN COMPUTE POOL {compute_pool}",
154
+ f"FROM {stage}",
155
+ f"SPECIFICATION_FILE = '{spec_path}'",
156
+ f"AUTO_RESUME = {auto_resume}",
157
+ ]
158
+
159
+ if min_instances:
160
+ query.append(f"MIN_INSTANCES = {min_instances}")
161
+
162
+ if max_instances:
163
+ query.append(f"MAX_INSTANCES = {max_instances}")
164
+
165
+ if query_warehouse:
166
+ query.append(f"QUERY_WAREHOUSE = {query_warehouse}")
167
+
168
+ if external_access_integrations:
169
+ external_access_integration_list = ",".join(
170
+ f"{e}" for e in external_access_integrations
171
+ )
172
+ query.append(
173
+ f"EXTERNAL_ACCESS_INTEGRATIONS = ({external_access_integration_list})"
174
+ )
175
+
176
+ if comment:
177
+ query.append(f"COMMENT = {comment}")
178
+
179
+ if tags:
180
+ tag_list = ",".join(
181
+ f"{t.name}={t.value_string_literal()}" for t in tags
182
+ )
183
+ query.append(f"WITH TAG ({tag_list})")
184
+
185
+ try:
186
+ return self.execute_query(strip_empty_lines(query))
187
+ except ProgrammingError as e:
188
+ handle_object_already_exists(e, ObjectType.SERVICE, service_name)
189
+
190
+ @staticmethod
191
+ def _upload_artifacts(
192
+ stage_manager: StageManager,
193
+ service_project_paths: ServiceProjectPaths,
194
+ artifacts: Artifacts,
195
+ stage: str,
196
+ ):
197
+ if not artifacts:
198
+ raise ValueError("Service needs to have artifacts to deploy")
199
+
200
+ bundle_map = bundle_artifacts(service_project_paths, artifacts)
201
+ for absolute_src, absolute_dest in bundle_map.all_mappings(
202
+ absolute=True, expand_directories=True
203
+ ):
204
+ # We treat the bundle/service root as deploy root
205
+ stage_path = StagePath.from_stage_str(stage) / (
206
+ absolute_dest.relative_to(service_project_paths.bundle_root).parent
207
+ )
208
+ stage_manager.put(
209
+ local_path=absolute_dest, stage_path=stage_path, overwrite=True
210
+ )
211
+
98
212
  def execute_job(
99
213
  self,
100
214
  job_service_name: str,
@@ -0,0 +1,6 @@
1
+ from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
2
+ from snowflake.cli.api.entities.common import EntityBase
3
+
4
+
5
+ class ServiceEntity(EntityBase[ServiceEntityModel]):
6
+ pass
@@ -0,0 +1,45 @@
1
+ from pathlib import Path
2
+ from typing import List, Literal, Optional
3
+
4
+ from pydantic import Field, field_validator
5
+ from snowflake.cli._plugins.object.common import Tag
6
+ from snowflake.cli.api.project.schemas.entities.common import (
7
+ EntityModelBaseWithArtifacts,
8
+ ExternalAccessBaseModel,
9
+ )
10
+ from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
11
+ from snowflake.cli.api.project.util import to_string_literal
12
+
13
+
14
+ class ServiceEntityModel(EntityModelBaseWithArtifacts, ExternalAccessBaseModel):
15
+ type: Literal["service"] = DiscriminatorField() # noqa: A003
16
+ stage: str = Field(
17
+ title="Stage where the service specification file is located", default=None
18
+ )
19
+ compute_pool: str = Field(title="Compute pool to run the service on", default=None)
20
+ spec_file: Path = Field(
21
+ title="Path to service specification file on stage", default=None
22
+ )
23
+ min_instances: Optional[int] = Field(
24
+ title="Minimum number of instances", default=1, ge=1
25
+ )
26
+ max_instances: Optional[int] = Field(
27
+ title="Maximum number of instances", default=None, ge=1
28
+ )
29
+ auto_resume: bool = Field(
30
+ title="The service will automatically resume when a service function or ingress is called.",
31
+ default=True,
32
+ )
33
+ query_warehouse: Optional[str] = Field(
34
+ title="Warehouse to use if a service container connects to Snowflake to execute a query without explicitly specifying a warehouse to use",
35
+ default=None,
36
+ )
37
+ tags: Optional[List[Tag]] = Field(title="Tag for the service", default=None)
38
+ comment: Optional[str] = Field(title="Comment for the service", default=None)
39
+
40
+ @field_validator("comment")
41
+ @classmethod
42
+ def _convert_artifacts(cls, comment: Optional[str]):
43
+ if comment:
44
+ return to_string_literal(comment)
45
+ return comment
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ from snowflake.cli.api.project.project_paths import ProjectPaths, bundle_root
5
+
6
+
7
+ @dataclass
8
+ class ServiceProjectPaths(ProjectPaths):
9
+ """
10
+ This class allows you to manage files paths related to given project.
11
+ """
12
+
13
+ @property
14
+ def bundle_root(self) -> Path:
15
+ return bundle_root(self.project_root, "service")
@@ -561,7 +561,7 @@ class StageManager(SqlExecutionMixin):
561
561
  )
562
562
 
563
563
  parsed_variables = parse_key_value_variables(variables)
564
- sql_variables = self._parse_execute_variables(parsed_variables)
564
+ sql_variables = self.parse_execute_variables(parsed_variables)
565
565
  python_variables = self._parse_python_variables(parsed_variables)
566
566
  results = []
567
567
 
@@ -663,7 +663,7 @@ class StageManager(SqlExecutionMixin):
663
663
  return [f for f in files if Path(f).suffix in EXECUTE_SUPPORTED_FILES_FORMATS]
664
664
 
665
665
  @staticmethod
666
- def _parse_execute_variables(variables: List[Variable]) -> Optional[str]:
666
+ def parse_execute_variables(variables: List[Variable]) -> Optional[str]:
667
667
  if not variables:
668
668
  return None
669
669
  query_parameters = [f"{v.key}=>{v.value}" for v in variables]