snowflake-cli 3.4.1__py3-none-any.whl → 3.6.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 (73) hide show
  1. snowflake/cli/__about__.py +13 -1
  2. snowflake/cli/_app/cli_app.py +1 -10
  3. snowflake/cli/_app/commands_registration/builtin_plugins.py +7 -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/_app/snow_connector.py +5 -4
  8. snowflake/cli/_app/telemetry.py +3 -15
  9. snowflake/cli/_app/version_check.py +4 -4
  10. snowflake/cli/_plugins/auth/__init__.py +11 -0
  11. snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
  12. snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
  13. snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
  14. snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
  15. snowflake/cli/_plugins/connection/commands.py +79 -5
  16. snowflake/cli/_plugins/helpers/commands.py +3 -4
  17. snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
  19. snowflake/cli/_plugins/notebook/commands.py +3 -4
  20. snowflake/cli/_plugins/object/command_aliases.py +3 -1
  21. snowflake/cli/_plugins/object/manager.py +4 -2
  22. snowflake/cli/_plugins/plugin/commands.py +79 -0
  23. snowflake/cli/_plugins/plugin/manager.py +74 -0
  24. snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
  25. snowflake/cli/_plugins/project/__init__.py +0 -0
  26. snowflake/cli/_plugins/project/commands.py +173 -0
  27. snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -0
  28. snowflake/cli/_plugins/project/manager.py +76 -0
  29. snowflake/cli/_plugins/project/plugin_spec.py +30 -0
  30. snowflake/cli/_plugins/project/project_entity_model.py +40 -0
  31. snowflake/cli/_plugins/snowpark/commands.py +2 -1
  32. snowflake/cli/_plugins/spcs/compute_pool/commands.py +70 -10
  33. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
  34. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
  35. snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
  36. snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
  37. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
  38. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
  39. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  40. snowflake/cli/_plugins/spcs/services/commands.py +53 -0
  41. snowflake/cli/_plugins/spcs/services/manager.py +114 -0
  42. snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
  43. snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
  44. snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
  45. snowflake/cli/_plugins/sql/manager.py +42 -51
  46. snowflake/cli/_plugins/sql/source_reader.py +230 -0
  47. snowflake/cli/_plugins/stage/manager.py +10 -4
  48. snowflake/cli/_plugins/streamlit/commands.py +9 -24
  49. snowflake/cli/_plugins/streamlit/manager.py +5 -36
  50. snowflake/cli/api/artifacts/upload.py +51 -0
  51. snowflake/cli/api/commands/flags.py +35 -10
  52. snowflake/cli/api/commands/snow_typer.py +12 -0
  53. snowflake/cli/api/commands/utils.py +2 -0
  54. snowflake/cli/api/config.py +15 -10
  55. snowflake/cli/api/constants.py +2 -0
  56. snowflake/cli/api/errno.py +1 -0
  57. snowflake/cli/api/exceptions.py +15 -1
  58. snowflake/cli/api/feature_flags.py +2 -0
  59. snowflake/cli/api/plugins/plugin_config.py +43 -4
  60. snowflake/cli/api/project/definition_helper.py +31 -0
  61. snowflake/cli/api/project/schemas/entities/entities.py +26 -0
  62. snowflake/cli/api/rest_api.py +2 -3
  63. snowflake/cli/{_app → api}/secret.py +4 -1
  64. snowflake/cli/api/secure_path.py +16 -4
  65. snowflake/cli/api/sql_execution.py +7 -3
  66. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +12 -12
  67. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +71 -50
  68. snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
  69. snowflake/cli/api/__init__.py +0 -48
  70. /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
  71. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
  72. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
  73. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(
@@ -79,9 +88,17 @@ MaxNodesOption = OverrideableOption(
79
88
  _AUTO_RESUME_HELP = "The compute pool will automatically resume when a service or job is submitted to it."
80
89
 
81
90
  AutoResumeOption = OverrideableOption(
82
- True,
83
- "--auto-resume/--no-auto-resume",
91
+ False,
92
+ "--auto-resume",
93
+ help=_AUTO_RESUME_HELP,
94
+ mutually_exclusive=["no_auto_resume"],
95
+ )
96
+
97
+ NoAutoResumeOption = OverrideableOption(
98
+ False,
99
+ "--no-auto-resume",
84
100
  help=_AUTO_RESUME_HELP,
101
+ mutually_exclusive=["auto_resume"],
85
102
  )
86
103
 
87
104
  _AUTO_SUSPEND_SECS_HELP = "Number of seconds of inactivity after which you want Snowflake to automatically suspend the compute pool."
@@ -117,12 +134,14 @@ def create(
117
134
  min_nodes: int = MinNodesOption(),
118
135
  max_nodes: Optional[int] = MaxNodesOption(),
119
136
  auto_resume: bool = AutoResumeOption(),
137
+ no_auto_resume: bool = NoAutoResumeOption(),
120
138
  initially_suspended: bool = typer.Option(
121
139
  False,
122
140
  "--init-suspend/--no-init-suspend",
123
141
  help="Starts the compute pool in a suspended state.",
124
142
  ),
125
143
  auto_suspend_secs: int = AutoSuspendSecsOption(),
144
+ tags: Optional[List[Tag]] = TagOption(help="Tag for the compute pool."),
126
145
  comment: Optional[str] = CommentOption(help=_COMMENT_HELP),
127
146
  if_not_exists: bool = IfNotExistsOption(),
128
147
  **options,
@@ -130,21 +149,60 @@ def create(
130
149
  """
131
150
  Creates a new compute pool.
132
151
  """
152
+ resume_option = True if auto_resume else False if no_auto_resume else True
133
153
  max_nodes = validate_and_set_instances(min_nodes, max_nodes, "nodes")
134
154
  cursor = ComputePoolManager().create(
135
155
  pool_name=name.identifier,
136
156
  min_nodes=min_nodes,
137
157
  max_nodes=max_nodes,
138
158
  instance_family=instance_family,
139
- auto_resume=auto_resume,
159
+ auto_resume=resume_option,
140
160
  initially_suspended=initially_suspended,
141
161
  auto_suspend_secs=auto_suspend_secs,
162
+ tags=tags,
142
163
  comment=comment,
143
164
  if_not_exists=if_not_exists,
144
165
  )
145
166
  return SingleQueryResult(cursor)
146
167
 
147
168
 
169
+ @app.command("deploy", requires_connection=True)
170
+ @with_project_definition()
171
+ def deploy(
172
+ entity_id: str = entity_argument("compute-pool"),
173
+ upgrade: bool = typer.Option(
174
+ False,
175
+ "--upgrade",
176
+ help="Updates the existing compute pool. Can update min_nodes, max_nodes, auto_resume, auto_suspend_seconds and comment.",
177
+ ),
178
+ **options,
179
+ ):
180
+ """
181
+ Deploys a compute pool from the project definition file.
182
+ """
183
+ compute_pool: ComputePoolEntityModel = get_entity_from_project_definition(
184
+ entity_type=ObjectType.COMPUTE_POOL, entity_id=entity_id
185
+ )
186
+ max_nodes = validate_and_set_instances(
187
+ compute_pool.min_nodes, compute_pool.max_nodes, "nodes"
188
+ )
189
+
190
+ cursor = ComputePoolManager().deploy(
191
+ pool_name=compute_pool.fqn.identifier,
192
+ min_nodes=compute_pool.min_nodes,
193
+ max_nodes=max_nodes,
194
+ instance_family=compute_pool.instance_family,
195
+ auto_resume=compute_pool.auto_resume,
196
+ initially_suspended=compute_pool.initially_suspended,
197
+ auto_suspend_seconds=compute_pool.auto_suspend_seconds,
198
+ tags=compute_pool.tags,
199
+ comment=compute_pool.comment,
200
+ upgrade=upgrade,
201
+ )
202
+
203
+ return SingleQueryResult(cursor)
204
+
205
+
148
206
  @app.command("stop-all", requires_connection=True)
149
207
  def stop_all(name: FQN = ComputePoolNameArgument, **options) -> CommandResult:
150
208
  """
@@ -175,7 +233,8 @@ def set_property(
175
233
  name: FQN = ComputePoolNameArgument,
176
234
  min_nodes: Optional[int] = MinNodesOption(default=None, show_default=False),
177
235
  max_nodes: Optional[int] = MaxNodesOption(show_default=False),
178
- auto_resume: Optional[bool] = AutoResumeOption(default=None, show_default=False),
236
+ auto_resume: bool = AutoResumeOption(default=None, show_default=False),
237
+ no_auto_resume: bool = NoAutoResumeOption(default=None, show_default=False),
179
238
  auto_suspend_secs: Optional[int] = AutoSuspendSecsOption(
180
239
  default=None, show_default=False
181
240
  ),
@@ -187,11 +246,12 @@ def set_property(
187
246
  """
188
247
  Sets one or more properties for the compute pool.
189
248
  """
249
+ resume_option = True if auto_resume else False if no_auto_resume else None
190
250
  cursor = ComputePoolManager().set_property(
191
251
  pool_name=name.identifier,
192
252
  min_nodes=min_nodes,
193
253
  max_nodes=max_nodes,
194
- auto_resume=auto_resume,
254
+ auto_resume=resume_option,
195
255
  auto_suspend_secs=auto_suspend_secs,
196
256
  comment=comment,
197
257
  )
@@ -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")