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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +1 -10
- snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_plugins/connection/commands.py +2 -4
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/notebook/commands.py +3 -4
- snowflake/cli/_plugins/plugin/commands.py +79 -0
- snowflake/cli/_plugins/plugin/manager.py +74 -0
- snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/__init__.py +0 -0
- snowflake/cli/_plugins/project/commands.py +157 -0
- snowflake/cli/{_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 +53 -5
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +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/stage/manager.py +2 -2
- 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 +24 -9
- 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/exceptions.py +8 -1
- snowflake/cli/api/feature_flags.py +1 -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-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +9 -9
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +51 -36
- 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.5.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
24
|
+
from snowflake.cli._plugins.object.common import CommentOption, Tag, TagOption
|
|
25
|
+
from snowflake.cli._plugins.spcs.common import validate_and_set_instances
|
|
26
|
+
from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity_model import (
|
|
27
|
+
ComputePoolEntityModel,
|
|
27
28
|
)
|
|
28
29
|
from snowflake.cli._plugins.spcs.compute_pool.manager import ComputePoolManager
|
|
30
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
29
31
|
from snowflake.cli.api.commands.flags import (
|
|
30
32
|
IfNotExistsOption,
|
|
31
33
|
OverrideableOption,
|
|
34
|
+
entity_argument,
|
|
32
35
|
identifier_argument,
|
|
33
36
|
like_option,
|
|
34
37
|
)
|
|
35
38
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
36
39
|
from snowflake.cli.api.constants import ObjectType
|
|
37
40
|
from snowflake.cli.api.identifiers import FQN
|
|
38
|
-
from snowflake.cli.api.output.types import
|
|
41
|
+
from snowflake.cli.api.output.types import (
|
|
42
|
+
CommandResult,
|
|
43
|
+
SingleQueryResult,
|
|
44
|
+
)
|
|
45
|
+
from snowflake.cli.api.project.definition_helper import (
|
|
46
|
+
get_entity_from_project_definition,
|
|
47
|
+
)
|
|
39
48
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
40
49
|
|
|
41
50
|
app = SnowTyperFactory(
|
|
@@ -123,6 +132,7 @@ def create(
|
|
|
123
132
|
help="Starts the compute pool in a suspended state.",
|
|
124
133
|
),
|
|
125
134
|
auto_suspend_secs: int = AutoSuspendSecsOption(),
|
|
135
|
+
tags: Optional[List[Tag]] = TagOption(help="Tag for the compute pool."),
|
|
126
136
|
comment: Optional[str] = CommentOption(help=_COMMENT_HELP),
|
|
127
137
|
if_not_exists: bool = IfNotExistsOption(),
|
|
128
138
|
**options,
|
|
@@ -139,12 +149,50 @@ def create(
|
|
|
139
149
|
auto_resume=auto_resume,
|
|
140
150
|
initially_suspended=initially_suspended,
|
|
141
151
|
auto_suspend_secs=auto_suspend_secs,
|
|
152
|
+
tags=tags,
|
|
142
153
|
comment=comment,
|
|
143
154
|
if_not_exists=if_not_exists,
|
|
144
155
|
)
|
|
145
156
|
return SingleQueryResult(cursor)
|
|
146
157
|
|
|
147
158
|
|
|
159
|
+
@app.command("deploy", requires_connection=True)
|
|
160
|
+
@with_project_definition()
|
|
161
|
+
def deploy(
|
|
162
|
+
entity_id: str = entity_argument("compute-pool"),
|
|
163
|
+
upgrade: bool = typer.Option(
|
|
164
|
+
False,
|
|
165
|
+
"--upgrade",
|
|
166
|
+
help="Updates the existing compute pool. Can update min_nodes, max_nodes, auto_resume, auto_suspend_seconds and comment.",
|
|
167
|
+
),
|
|
168
|
+
**options,
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Deploys a compute pool from the project definition file.
|
|
172
|
+
"""
|
|
173
|
+
compute_pool: ComputePoolEntityModel = get_entity_from_project_definition(
|
|
174
|
+
entity_type=ObjectType.COMPUTE_POOL, entity_id=entity_id
|
|
175
|
+
)
|
|
176
|
+
max_nodes = validate_and_set_instances(
|
|
177
|
+
compute_pool.min_nodes, compute_pool.max_nodes, "nodes"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
cursor = ComputePoolManager().deploy(
|
|
181
|
+
pool_name=compute_pool.fqn.identifier,
|
|
182
|
+
min_nodes=compute_pool.min_nodes,
|
|
183
|
+
max_nodes=max_nodes,
|
|
184
|
+
instance_family=compute_pool.instance_family,
|
|
185
|
+
auto_resume=compute_pool.auto_resume,
|
|
186
|
+
initially_suspended=compute_pool.initially_suspended,
|
|
187
|
+
auto_suspend_seconds=compute_pool.auto_suspend_seconds,
|
|
188
|
+
tags=compute_pool.tags,
|
|
189
|
+
comment=compute_pool.comment,
|
|
190
|
+
upgrade=upgrade,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return SingleQueryResult(cursor)
|
|
194
|
+
|
|
195
|
+
|
|
148
196
|
@app.command("stop-all", requires_connection=True)
|
|
149
197
|
def stop_all(name: FQN = ComputePoolNameArgument, **options) -> CommandResult:
|
|
150
198
|
"""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import List, Literal, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_validator
|
|
4
|
+
from snowflake.cli._plugins.object.common import Tag
|
|
5
|
+
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
6
|
+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
|
|
7
|
+
from snowflake.cli.api.project.util import to_string_literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComputePoolEntityModel(EntityModelBase):
|
|
11
|
+
type: Literal["compute-pool"] = DiscriminatorField() # noqa: A003
|
|
12
|
+
min_nodes: Optional[int] = Field(title="Minimum number of nodes", default=1, ge=1)
|
|
13
|
+
max_nodes: Optional[int] = Field(
|
|
14
|
+
title="Maximum number of nodes", default=None, ge=1
|
|
15
|
+
)
|
|
16
|
+
instance_family: str = Field(title="Name of the instance family", default=None)
|
|
17
|
+
auto_resume: Optional[bool] = Field(
|
|
18
|
+
title="The compute pool will automatically resume when a service or job is submitted to it",
|
|
19
|
+
default=True,
|
|
20
|
+
)
|
|
21
|
+
initially_suspended: Optional[bool] = Field(
|
|
22
|
+
title="Starts the compute pool in a suspended state", default=False
|
|
23
|
+
)
|
|
24
|
+
auto_suspend_seconds: Optional[int] = Field(
|
|
25
|
+
title="Number of seconds of inactivity after which you want Snowflake to automatically suspend the compute pool",
|
|
26
|
+
default=3600,
|
|
27
|
+
ge=1,
|
|
28
|
+
)
|
|
29
|
+
comment: Optional[str] = Field(title="Comment for the compute pool", default=None)
|
|
30
|
+
tags: Optional[List[Tag]] = Field(title="Tag for the compute pool", default=None)
|
|
31
|
+
|
|
32
|
+
@field_validator("comment")
|
|
33
|
+
@classmethod
|
|
34
|
+
def _convert_artifacts(cls, comment: Optional[str]):
|
|
35
|
+
if comment:
|
|
36
|
+
return to_string_literal(comment)
|
|
37
|
+
return comment
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from typing import List, Optional
|
|
18
18
|
|
|
19
|
+
from snowflake.cli._plugins.object.common import Tag
|
|
19
20
|
from snowflake.cli._plugins.spcs.common import (
|
|
20
21
|
NoPropertiesProvidedError,
|
|
21
22
|
handle_object_already_exists,
|
|
@@ -37,9 +38,11 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
37
38
|
auto_resume: bool,
|
|
38
39
|
initially_suspended: bool,
|
|
39
40
|
auto_suspend_secs: int,
|
|
41
|
+
tags: Optional[List[Tag]],
|
|
40
42
|
comment: Optional[str],
|
|
41
43
|
if_not_exists: bool,
|
|
42
44
|
) -> SnowflakeCursor:
|
|
45
|
+
|
|
43
46
|
create_statement = "CREATE COMPUTE POOL"
|
|
44
47
|
if if_not_exists:
|
|
45
48
|
create_statement = f"{create_statement} IF NOT EXISTS"
|
|
@@ -55,11 +58,52 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
55
58
|
if comment:
|
|
56
59
|
query.append(f"COMMENT = {comment}")
|
|
57
60
|
|
|
61
|
+
if tags:
|
|
62
|
+
tag_list = ",".join(f"{t.name}={t.value_string_literal()}" for t in tags)
|
|
63
|
+
query.append(f"WITH TAG ({tag_list})")
|
|
64
|
+
|
|
58
65
|
try:
|
|
59
66
|
return self.execute_query(strip_empty_lines(query))
|
|
60
67
|
except ProgrammingError as e:
|
|
61
68
|
handle_object_already_exists(e, ObjectType.COMPUTE_POOL, pool_name)
|
|
62
69
|
|
|
70
|
+
def deploy(
|
|
71
|
+
self,
|
|
72
|
+
pool_name: str,
|
|
73
|
+
min_nodes: int,
|
|
74
|
+
max_nodes: int,
|
|
75
|
+
instance_family: str,
|
|
76
|
+
auto_resume: bool,
|
|
77
|
+
initially_suspended: bool,
|
|
78
|
+
auto_suspend_seconds: int,
|
|
79
|
+
tags: Optional[List[Tag]],
|
|
80
|
+
comment: Optional[str],
|
|
81
|
+
upgrade: bool,
|
|
82
|
+
):
|
|
83
|
+
|
|
84
|
+
if upgrade:
|
|
85
|
+
return self.set_property(
|
|
86
|
+
pool_name=pool_name,
|
|
87
|
+
min_nodes=min_nodes,
|
|
88
|
+
max_nodes=max_nodes,
|
|
89
|
+
auto_resume=auto_resume,
|
|
90
|
+
auto_suspend_secs=auto_suspend_seconds,
|
|
91
|
+
comment=comment,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
return self.create(
|
|
95
|
+
pool_name=pool_name,
|
|
96
|
+
min_nodes=min_nodes,
|
|
97
|
+
max_nodes=max_nodes,
|
|
98
|
+
instance_family=instance_family,
|
|
99
|
+
auto_resume=auto_resume,
|
|
100
|
+
initially_suspended=initially_suspended,
|
|
101
|
+
auto_suspend_secs=auto_suspend_seconds,
|
|
102
|
+
tags=tags,
|
|
103
|
+
comment=comment,
|
|
104
|
+
if_not_exists=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
63
107
|
def stop(self, pool_name: str) -> SnowflakeCursor:
|
|
64
108
|
return self.execute_query(f"alter compute pool {pool_name} stop all")
|
|
65
109
|
|
|
@@ -95,6 +139,7 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
95
139
|
for property_name, value in property_pairs:
|
|
96
140
|
if value is not None:
|
|
97
141
|
query.append(f"{property_name} = {value}")
|
|
142
|
+
|
|
98
143
|
return self.execute_query(strip_empty_lines(query))
|
|
99
144
|
|
|
100
145
|
def unset_property(
|
|
@@ -26,9 +26,11 @@ from snowflake.cli._plugins.object.command_aliases import (
|
|
|
26
26
|
)
|
|
27
27
|
from snowflake.cli._plugins.spcs.image_registry.manager import RegistryManager
|
|
28
28
|
from snowflake.cli._plugins.spcs.image_repository.manager import ImageRepositoryManager
|
|
29
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
29
30
|
from snowflake.cli.api.commands.flags import (
|
|
30
31
|
IfNotExistsOption,
|
|
31
32
|
ReplaceOption,
|
|
33
|
+
entity_argument,
|
|
32
34
|
identifier_argument,
|
|
33
35
|
like_option,
|
|
34
36
|
)
|
|
@@ -42,6 +44,9 @@ from snowflake.cli.api.output.types import (
|
|
|
42
44
|
QueryResult,
|
|
43
45
|
SingleQueryResult,
|
|
44
46
|
)
|
|
47
|
+
from snowflake.cli.api.project.definition_helper import (
|
|
48
|
+
get_entity_from_project_definition,
|
|
49
|
+
)
|
|
45
50
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
46
51
|
|
|
47
52
|
app = SnowTyperFactory(
|
|
@@ -94,6 +99,30 @@ def create(
|
|
|
94
99
|
)
|
|
95
100
|
|
|
96
101
|
|
|
102
|
+
@app.command(requires_connection=True)
|
|
103
|
+
@with_project_definition()
|
|
104
|
+
def deploy(
|
|
105
|
+
entity_id: str = entity_argument("image-repository"),
|
|
106
|
+
replace: bool = ReplaceOption(
|
|
107
|
+
help="Replace the image repository if it already exists."
|
|
108
|
+
),
|
|
109
|
+
**options,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Deploys a new image repository from snowflake.yml file.
|
|
113
|
+
"""
|
|
114
|
+
image_repository = get_entity_from_project_definition(
|
|
115
|
+
ObjectType.IMAGE_REPOSITORY, entity_id
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
cursor = ImageRepositoryManager().create(
|
|
119
|
+
name=image_repository.fqn.identifier,
|
|
120
|
+
if_not_exists=False,
|
|
121
|
+
replace=replace,
|
|
122
|
+
)
|
|
123
|
+
return SingleQueryResult(cursor)
|
|
124
|
+
|
|
125
|
+
|
|
97
126
|
@app.command("list-images", requires_connection=True)
|
|
98
127
|
def list_images(
|
|
99
128
|
name: FQN = REPO_NAME_ARGUMENT,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
4
|
+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ImageRepositoryEntityModel(EntityModelBase):
|
|
8
|
+
type: Literal["image-repository"] = DiscriminatorField() # noqa: A003
|
|
@@ -64,7 +64,7 @@ class ImageRepositoryManager(SqlExecutionMixin):
|
|
|
64
64
|
name: str,
|
|
65
65
|
if_not_exists: bool,
|
|
66
66
|
replace: bool,
|
|
67
|
-
):
|
|
67
|
+
) -> SnowflakeCursor:
|
|
68
68
|
if if_not_exists and replace:
|
|
69
69
|
raise ValueError(
|
|
70
70
|
"'replace' and 'if_not_exists' options are mutually exclusive for ImageRepositoryManager.create"
|
|
@@ -29,9 +29,16 @@ from snowflake.cli._plugins.spcs.common import (
|
|
|
29
29
|
validate_and_set_instances,
|
|
30
30
|
)
|
|
31
31
|
from snowflake.cli._plugins.spcs.services.manager import ServiceManager
|
|
32
|
+
from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
|
|
33
|
+
from snowflake.cli._plugins.spcs.services.service_project_paths import (
|
|
34
|
+
ServiceProjectPaths,
|
|
35
|
+
)
|
|
36
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
37
|
+
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
32
38
|
from snowflake.cli.api.commands.flags import (
|
|
33
39
|
IfNotExistsOption,
|
|
34
40
|
OverrideableOption,
|
|
41
|
+
entity_argument,
|
|
35
42
|
identifier_argument,
|
|
36
43
|
like_option,
|
|
37
44
|
)
|
|
@@ -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")
|
|
@@ -561,7 +561,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
561
561
|
)
|
|
562
562
|
|
|
563
563
|
parsed_variables = parse_key_value_variables(variables)
|
|
564
|
-
sql_variables = self.
|
|
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
|
|
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]
|