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.
- snowflake/cli/__about__.py +13 -1
- snowflake/cli/_app/cli_app.py +1 -10
- snowflake/cli/_app/commands_registration/builtin_plugins.py +7 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_app/snow_connector.py +5 -4
- snowflake/cli/_app/telemetry.py +3 -15
- snowflake/cli/_app/version_check.py +4 -4
- snowflake/cli/_plugins/auth/__init__.py +11 -0
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
- snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
- snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +79 -5
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
- snowflake/cli/_plugins/notebook/commands.py +3 -4
- snowflake/cli/_plugins/object/command_aliases.py +3 -1
- snowflake/cli/_plugins/object/manager.py +4 -2
- 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 +173 -0
- snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -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 +2 -1
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +70 -10
- 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 +53 -0
- 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/sql/manager.py +42 -51
- snowflake/cli/_plugins/sql/source_reader.py +230 -0
- snowflake/cli/_plugins/stage/manager.py +10 -4
- snowflake/cli/_plugins/streamlit/commands.py +9 -24
- snowflake/cli/_plugins/streamlit/manager.py +5 -36
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/commands/flags.py +35 -10
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +2 -0
- snowflake/cli/api/config.py +15 -10
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/exceptions.py +15 -1
- snowflake/cli/api/feature_flags.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_helper.py +31 -0
- snowflake/cli/api/project/schemas/entities/entities.py +26 -0
- snowflake/cli/api/rest_api.py +2 -3
- snowflake/cli/{_app → api}/secret.py +4 -1
- snowflake/cli/api/secure_path.py +16 -4
- snowflake/cli/api/sql_execution.py +7 -3
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +12 -12
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +71 -50
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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(
|
|
@@ -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
|
-
|
|
83
|
-
"--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=
|
|
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:
|
|
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=
|
|
254
|
+
auto_resume=resume_option,
|
|
195
255
|
auto_suspend_secs=auto_suspend_secs,
|
|
196
256
|
comment=comment,
|
|
197
257
|
)
|
|
@@ -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
|
)
|
|
@@ -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,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")
|