snowflake-cli 3.0.2__py3-none-any.whl → 3.2.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 +3 -0
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
- snowflake/cli/_app/telemetry.py +69 -4
- snowflake/cli/_plugins/connection/commands.py +152 -99
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +9 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +144 -188
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
- snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +63 -21
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
- snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
- snowflake/cli/_plugins/spcs/services/commands.py +100 -17
- snowflake/cli/_plugins/spcs/services/manager.py +108 -16
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +28 -19
- snowflake/cli/_plugins/stage/diff.py +17 -17
- snowflake/cli/_plugins/stage/manager.py +304 -84
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +27 -4
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +4 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +30 -9
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +36 -69
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +41 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +293 -77
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +40 -29
- snowflake/cli/api/stage_path.py +244 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -56,18 +56,18 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
56
56
|
query.append(f"COMMENT = {comment}")
|
|
57
57
|
|
|
58
58
|
try:
|
|
59
|
-
return self.
|
|
59
|
+
return self.execute_query(strip_empty_lines(query))
|
|
60
60
|
except ProgrammingError as e:
|
|
61
61
|
handle_object_already_exists(e, ObjectType.COMPUTE_POOL, pool_name)
|
|
62
62
|
|
|
63
63
|
def stop(self, pool_name: str) -> SnowflakeCursor:
|
|
64
|
-
return self.
|
|
64
|
+
return self.execute_query(f"alter compute pool {pool_name} stop all")
|
|
65
65
|
|
|
66
66
|
def suspend(self, pool_name: str) -> SnowflakeCursor:
|
|
67
|
-
return self.
|
|
67
|
+
return self.execute_query(f"alter compute pool {pool_name} suspend")
|
|
68
68
|
|
|
69
69
|
def resume(self, pool_name: str) -> SnowflakeCursor:
|
|
70
|
-
return self.
|
|
70
|
+
return self.execute_query(f"alter compute pool {pool_name} resume")
|
|
71
71
|
|
|
72
72
|
def set_property(
|
|
73
73
|
self,
|
|
@@ -95,7 +95,7 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
95
95
|
for property_name, value in property_pairs:
|
|
96
96
|
if value is not None:
|
|
97
97
|
query.append(f"{property_name} = {value}")
|
|
98
|
-
return self.
|
|
98
|
+
return self.execute_query(strip_empty_lines(query))
|
|
99
99
|
|
|
100
100
|
def unset_property(
|
|
101
101
|
self, pool_name: str, auto_resume: bool, auto_suspend_secs: bool, comment: bool
|
|
@@ -113,9 +113,7 @@ class ComputePoolManager(SqlExecutionMixin):
|
|
|
113
113
|
)
|
|
114
114
|
unset_list = [property_name for property_name, value in property_pairs if value]
|
|
115
115
|
query = f"alter compute pool {pool_name} unset {','.join(unset_list)}"
|
|
116
|
-
return self.
|
|
116
|
+
return self.execute_query(query)
|
|
117
117
|
|
|
118
118
|
def status(self, pool_name: str):
|
|
119
|
-
return self.
|
|
120
|
-
f"call system$get_compute_pool_status('{pool_name}')"
|
|
121
|
-
)
|
|
119
|
+
return self.execute_query(f"call system$get_compute_pool_status('{pool_name}')")
|
|
@@ -36,7 +36,7 @@ class RegistryManager(SqlExecutionMixin):
|
|
|
36
36
|
"""
|
|
37
37
|
Get token to authenticate with registry.
|
|
38
38
|
"""
|
|
39
|
-
self.
|
|
39
|
+
self.execute_query(
|
|
40
40
|
"alter session set PYTHON_CONNECTOR_QUERY_RESULT_FORMAT = 'json'"
|
|
41
41
|
).fetchall()
|
|
42
42
|
# disable session deletion
|
|
@@ -75,7 +75,7 @@ class RegistryManager(SqlExecutionMixin):
|
|
|
75
75
|
|
|
76
76
|
def get_registry_url(self) -> str:
|
|
77
77
|
repositories_query = "show image repositories in account"
|
|
78
|
-
result_set = self.
|
|
78
|
+
result_set = self.execute_query(repositories_query, cursor_class=DictCursor)
|
|
79
79
|
results = result_set.fetchall()
|
|
80
80
|
if len(results) == 0:
|
|
81
81
|
raise NoImageRepositoriesFoundError()
|
|
@@ -39,6 +39,7 @@ from snowflake.cli.api.identifiers import FQN
|
|
|
39
39
|
from snowflake.cli.api.output.types import (
|
|
40
40
|
CollectionResult,
|
|
41
41
|
MessageResult,
|
|
42
|
+
QueryResult,
|
|
42
43
|
SingleQueryResult,
|
|
43
44
|
)
|
|
44
45
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
@@ -99,44 +100,10 @@ def list_images(
|
|
|
99
100
|
**options,
|
|
100
101
|
) -> CollectionResult:
|
|
101
102
|
"""Lists images in the given repository."""
|
|
102
|
-
|
|
103
|
-
database = repository_manager.get_database()
|
|
104
|
-
schema = repository_manager.get_schema()
|
|
105
|
-
url = repository_manager.get_repository_url(name.identifier)
|
|
106
|
-
api_url = repository_manager.get_repository_api_url(url)
|
|
107
|
-
bearer_login = RegistryManager().login_to_registry(api_url)
|
|
108
|
-
repos = []
|
|
109
|
-
query: Optional[str] = f"{api_url}/_catalog?n=10"
|
|
110
|
-
|
|
111
|
-
while query:
|
|
112
|
-
# Make paginated catalog requests
|
|
113
|
-
response = requests.get(
|
|
114
|
-
query, headers={"Authorization": f"Bearer {bearer_login}"}
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
if response.status_code != 200:
|
|
118
|
-
raise ClickException(f"Call to the registry failed {response.text}")
|
|
119
|
-
|
|
120
|
-
data = json.loads(response.text)
|
|
121
|
-
if "repositories" in data:
|
|
122
|
-
repos.extend(data["repositories"])
|
|
123
|
-
|
|
124
|
-
if "Link" in response.headers:
|
|
125
|
-
# There are more results
|
|
126
|
-
query = f"{api_url}/_catalog?n=10&last={repos[-1]}"
|
|
127
|
-
else:
|
|
128
|
-
query = None
|
|
129
|
-
|
|
130
|
-
images = []
|
|
131
|
-
for repo in repos:
|
|
132
|
-
prefix = f"/{database}/{schema}/{name}/"
|
|
133
|
-
repo = repo.replace("baserepo/", prefix)
|
|
134
|
-
images.append({"image": repo})
|
|
135
|
-
|
|
136
|
-
return CollectionResult(images)
|
|
103
|
+
return QueryResult(ImageRepositoryManager().list_images(name.identifier))
|
|
137
104
|
|
|
138
105
|
|
|
139
|
-
@app.command("list-tags", requires_connection=True)
|
|
106
|
+
@app.command("list-tags", requires_connection=True, deprecated=True)
|
|
140
107
|
def list_tags(
|
|
141
108
|
name: FQN = REPO_NAME_ARGUMENT,
|
|
142
109
|
image_name: str = typer.Option(
|
|
@@ -149,7 +116,7 @@ def list_tags(
|
|
|
149
116
|
),
|
|
150
117
|
**options,
|
|
151
118
|
) -> CollectionResult:
|
|
152
|
-
"""Lists tags for the given image in a repository."""
|
|
119
|
+
"""Lists tags for the given image in a repository. This command is deprecated and will be removed in a future release. Use `list-images` instead."""
|
|
153
120
|
|
|
154
121
|
repository_manager = ImageRepositoryManager()
|
|
155
122
|
url = repository_manager.get_repository_url(name.identifier)
|
|
@@ -18,6 +18,7 @@ from snowflake.cli._plugins.spcs.common import handle_object_already_exists
|
|
|
18
18
|
from snowflake.cli.api.constants import ObjectType
|
|
19
19
|
from snowflake.cli.api.identifiers import FQN
|
|
20
20
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
21
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
21
22
|
from snowflake.connector.errors import ProgrammingError
|
|
22
23
|
|
|
23
24
|
|
|
@@ -32,7 +33,6 @@ class ImageRepositoryManager(SqlExecutionMixin):
|
|
|
32
33
|
return self._conn.role
|
|
33
34
|
|
|
34
35
|
def get_repository_url(self, repo_name: str, with_scheme: bool = True):
|
|
35
|
-
|
|
36
36
|
repo_row = self.show_specific_object(
|
|
37
37
|
"image repositories", repo_name, check_schema=True
|
|
38
38
|
)
|
|
@@ -82,3 +82,6 @@ class ImageRepositoryManager(SqlExecutionMixin):
|
|
|
82
82
|
handle_object_already_exists(
|
|
83
83
|
e, ObjectType.IMAGE_REPOSITORY, name, replace_available=True
|
|
84
84
|
)
|
|
85
|
+
|
|
86
|
+
def list_images(self, repo_name: str) -> SnowflakeCursor:
|
|
87
|
+
return self.execute_query(f"show images in image repository {repo_name}")
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import itertools
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import List, Optional
|
|
19
|
+
from typing import Generator, Iterable, List, Optional, cast
|
|
20
20
|
|
|
21
21
|
import typer
|
|
22
22
|
from click import ClickException
|
|
@@ -26,7 +26,6 @@ from snowflake.cli._plugins.object.command_aliases import (
|
|
|
26
26
|
)
|
|
27
27
|
from snowflake.cli._plugins.object.common import CommentOption, Tag, TagOption
|
|
28
28
|
from snowflake.cli._plugins.spcs.common import (
|
|
29
|
-
print_log_lines,
|
|
30
29
|
validate_and_set_instances,
|
|
31
30
|
)
|
|
32
31
|
from snowflake.cli._plugins.spcs.services.manager import ServiceManager
|
|
@@ -38,12 +37,15 @@ from snowflake.cli.api.commands.flags import (
|
|
|
38
37
|
)
|
|
39
38
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
40
39
|
from snowflake.cli.api.constants import ObjectType
|
|
40
|
+
from snowflake.cli.api.exceptions import IncompatibleParametersError
|
|
41
41
|
from snowflake.cli.api.identifiers import FQN
|
|
42
42
|
from snowflake.cli.api.output.types import (
|
|
43
43
|
CommandResult,
|
|
44
|
+
MessageResult,
|
|
44
45
|
QueryJsonValueResult,
|
|
45
46
|
QueryResult,
|
|
46
47
|
SingleQueryResult,
|
|
48
|
+
StreamResult,
|
|
47
49
|
)
|
|
48
50
|
from snowflake.cli.api.project.util import is_valid_object_name
|
|
49
51
|
|
|
@@ -77,6 +79,7 @@ SpecPathOption = typer.Option(
|
|
|
77
79
|
exists=True,
|
|
78
80
|
show_default=False,
|
|
79
81
|
)
|
|
82
|
+
DEFAULT_NUM_LINES = 500
|
|
80
83
|
|
|
81
84
|
_MIN_INSTANCES_HELP = "Minimum number of service instances to run."
|
|
82
85
|
MinInstancesOption = OverrideableOption(
|
|
@@ -131,7 +134,7 @@ def create(
|
|
|
131
134
|
external_access_integrations: Optional[List[str]] = typer.Option(
|
|
132
135
|
None,
|
|
133
136
|
"--eai-name",
|
|
134
|
-
help="Identifies
|
|
137
|
+
help="Identifies external access integrations (EAI) that the service can access. This option may be specified multiple times for multiple EAIs.",
|
|
135
138
|
),
|
|
136
139
|
query_warehouse: Optional[str] = QueryWarehouseOption(),
|
|
137
140
|
tags: Optional[List[Tag]] = TagOption(help="Tag for the service."),
|
|
@@ -174,7 +177,7 @@ def execute_job(
|
|
|
174
177
|
external_access_integrations: Optional[List[str]] = typer.Option(
|
|
175
178
|
None,
|
|
176
179
|
"--eai-name",
|
|
177
|
-
help="Identifies
|
|
180
|
+
help="Identifies external access integrations (EAI) that the job service can access. This option may be specified multiple times for multiple EAIs.",
|
|
178
181
|
),
|
|
179
182
|
query_warehouse: Optional[str] = QueryWarehouseOption(),
|
|
180
183
|
comment: Optional[str] = CommentOption(help=_COMMENT_HELP),
|
|
@@ -194,10 +197,10 @@ def execute_job(
|
|
|
194
197
|
return SingleQueryResult(cursor)
|
|
195
198
|
|
|
196
199
|
|
|
197
|
-
@app.command(requires_connection=True)
|
|
200
|
+
@app.command(requires_connection=True, deprecated=True)
|
|
198
201
|
def status(name: FQN = ServiceNameArgument, **options) -> CommandResult:
|
|
199
202
|
"""
|
|
200
|
-
Retrieves the status of a service.
|
|
203
|
+
Retrieves the status of a service. This command is deprecated and will be removed in a future release. Use `describe` instead to get service status and use `list-instances` and `list-containers` to get more detailed information about service instances and containers.
|
|
201
204
|
"""
|
|
202
205
|
cursor = ServiceManager().status(service_name=name.identifier)
|
|
203
206
|
return QueryJsonValueResult(cursor)
|
|
@@ -219,22 +222,70 @@ def logs(
|
|
|
219
222
|
show_default=False,
|
|
220
223
|
),
|
|
221
224
|
num_lines: int = typer.Option(
|
|
222
|
-
|
|
225
|
+
DEFAULT_NUM_LINES, "--num-lines", help="Number of lines to retrieve."
|
|
226
|
+
),
|
|
227
|
+
previous_logs: bool = typer.Option(
|
|
228
|
+
False,
|
|
229
|
+
"--previous-logs",
|
|
230
|
+
help="Retrieve logs from the last terminated container.",
|
|
231
|
+
is_flag=True,
|
|
232
|
+
),
|
|
233
|
+
since_timestamp: Optional[str] = typer.Option(
|
|
234
|
+
"", "--since", help="Start log retrieval from a specified UTC timestamp."
|
|
235
|
+
),
|
|
236
|
+
include_timestamps: bool = typer.Option(
|
|
237
|
+
False, "--include-timestamps", help="Include timestamps in logs.", is_flag=True
|
|
238
|
+
),
|
|
239
|
+
follow: bool = typer.Option(
|
|
240
|
+
False, "--follow", help="Stream logs in real-time.", is_flag=True
|
|
241
|
+
),
|
|
242
|
+
follow_interval: int = typer.Option(
|
|
243
|
+
2,
|
|
244
|
+
"--follow-interval",
|
|
245
|
+
help="Set custom polling intervals for log streaming (--follow flag) in seconds.",
|
|
223
246
|
),
|
|
224
247
|
**options,
|
|
225
248
|
):
|
|
226
249
|
"""
|
|
227
250
|
Retrieves local logs from a service container.
|
|
228
251
|
"""
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
if follow:
|
|
253
|
+
if num_lines != DEFAULT_NUM_LINES:
|
|
254
|
+
raise IncompatibleParametersError(["--follow", "--num-lines"])
|
|
255
|
+
if previous_logs:
|
|
256
|
+
raise IncompatibleParametersError(["--follow", "--previous-logs"])
|
|
257
|
+
|
|
258
|
+
manager = ServiceManager()
|
|
259
|
+
|
|
260
|
+
if follow:
|
|
261
|
+
stream: Iterable[CommandResult] = (
|
|
262
|
+
MessageResult(log_batch)
|
|
263
|
+
for log_batch in manager.stream_logs(
|
|
264
|
+
service_name=name.identifier,
|
|
265
|
+
container_name=container_name,
|
|
266
|
+
instance_id=instance_id,
|
|
267
|
+
num_lines=num_lines,
|
|
268
|
+
since_timestamp=since_timestamp,
|
|
269
|
+
include_timestamps=include_timestamps,
|
|
270
|
+
interval_seconds=follow_interval,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
stream = itertools.chain(stream, [MessageResult("")])
|
|
274
|
+
else:
|
|
275
|
+
stream = (
|
|
276
|
+
MessageResult(log)
|
|
277
|
+
for log in manager.logs(
|
|
278
|
+
service_name=name.identifier,
|
|
279
|
+
container_name=container_name,
|
|
280
|
+
instance_id=instance_id,
|
|
281
|
+
num_lines=num_lines,
|
|
282
|
+
previous_logs=previous_logs,
|
|
283
|
+
since_timestamp=since_timestamp,
|
|
284
|
+
include_timestamps=include_timestamps,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return StreamResult(cast(Generator[CommandResult, None, None], stream))
|
|
238
289
|
|
|
239
290
|
|
|
240
291
|
@app.command(requires_connection=True)
|
|
@@ -259,6 +310,32 @@ def list_endpoints(name: FQN = ServiceNameArgument, **options):
|
|
|
259
310
|
return QueryResult(ServiceManager().list_endpoints(service_name=name.identifier))
|
|
260
311
|
|
|
261
312
|
|
|
313
|
+
@app.command("list-instances", requires_connection=True)
|
|
314
|
+
def list_service_instances(name: FQN = ServiceNameArgument, **options) -> CommandResult:
|
|
315
|
+
"""
|
|
316
|
+
Lists all service instances in a service.
|
|
317
|
+
"""
|
|
318
|
+
return QueryResult(ServiceManager().list_instances(service_name=name.identifier))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.command("list-containers", requires_connection=True)
|
|
322
|
+
def list_service_containers(
|
|
323
|
+
name: FQN = ServiceNameArgument, **options
|
|
324
|
+
) -> CommandResult:
|
|
325
|
+
"""
|
|
326
|
+
Lists all service containers in a service.
|
|
327
|
+
"""
|
|
328
|
+
return QueryResult(ServiceManager().list_containers(service_name=name.identifier))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("list-roles", requires_connection=True)
|
|
332
|
+
def list_service_roles(name: FQN = ServiceNameArgument, **options) -> CommandResult:
|
|
333
|
+
"""
|
|
334
|
+
Lists all service roles in a service.
|
|
335
|
+
"""
|
|
336
|
+
return QueryResult(ServiceManager().list_roles(service_name=name.identifier))
|
|
337
|
+
|
|
338
|
+
|
|
262
339
|
@app.command(requires_connection=True)
|
|
263
340
|
def suspend(name: FQN = ServiceNameArgument, **options) -> CommandResult:
|
|
264
341
|
"""
|
|
@@ -282,6 +359,11 @@ def set_property(
|
|
|
282
359
|
max_instances: Optional[int] = MaxInstancesOption(show_default=False),
|
|
283
360
|
query_warehouse: Optional[str] = QueryWarehouseOption(show_default=False),
|
|
284
361
|
auto_resume: Optional[bool] = AutoResumeOption(default=None, show_default=False),
|
|
362
|
+
external_access_integrations: Optional[List[str]] = typer.Option(
|
|
363
|
+
None,
|
|
364
|
+
"--eai-name",
|
|
365
|
+
help="Identifies external access integrations (EAI) that the service can access. This option may be specified multiple times for multiple EAIs.",
|
|
366
|
+
),
|
|
285
367
|
comment: Optional[str] = CommentOption(help=_COMMENT_HELP, show_default=False),
|
|
286
368
|
**options,
|
|
287
369
|
):
|
|
@@ -294,6 +376,7 @@ def set_property(
|
|
|
294
376
|
max_instances=max_instances,
|
|
295
377
|
query_warehouse=query_warehouse,
|
|
296
378
|
auto_resume=auto_resume,
|
|
379
|
+
external_access_integrations=external_access_integrations,
|
|
297
380
|
comment=comment,
|
|
298
381
|
)
|
|
299
382
|
return SingleQueryResult(cursor)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import time
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import List, Optional
|
|
20
21
|
|
|
@@ -22,7 +23,9 @@ import yaml
|
|
|
22
23
|
from snowflake.cli._plugins.object.common import Tag
|
|
23
24
|
from snowflake.cli._plugins.spcs.common import (
|
|
24
25
|
NoPropertiesProvidedError,
|
|
26
|
+
filter_log_timestamp,
|
|
25
27
|
handle_object_already_exists,
|
|
28
|
+
new_logs_only,
|
|
26
29
|
strip_empty_lines,
|
|
27
30
|
)
|
|
28
31
|
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
|
|
@@ -81,7 +84,7 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
81
84
|
query.append(f"WITH TAG ({tag_list})")
|
|
82
85
|
|
|
83
86
|
try:
|
|
84
|
-
return self.
|
|
87
|
+
return self.execute_query(strip_empty_lines(query))
|
|
85
88
|
except ProgrammingError as e:
|
|
86
89
|
handle_object_already_exists(e, ObjectType.SERVICE, service_name)
|
|
87
90
|
|
|
@@ -119,7 +122,7 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
119
122
|
query.append(f"COMMENT = {comment}")
|
|
120
123
|
|
|
121
124
|
try:
|
|
122
|
-
return self.
|
|
125
|
+
return self.execute_query(strip_empty_lines(query))
|
|
123
126
|
except ProgrammingError as e:
|
|
124
127
|
handle_object_already_exists(e, ObjectType.SERVICE, job_service_name)
|
|
125
128
|
|
|
@@ -130,28 +133,94 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
130
133
|
return json.dumps(data)
|
|
131
134
|
|
|
132
135
|
def status(self, service_name: str) -> SnowflakeCursor:
|
|
133
|
-
return self.
|
|
136
|
+
return self.execute_query(f"CALL SYSTEM$GET_SERVICE_STATUS('{service_name}')")
|
|
134
137
|
|
|
135
138
|
def logs(
|
|
136
|
-
self,
|
|
139
|
+
self,
|
|
140
|
+
service_name: str,
|
|
141
|
+
instance_id: str,
|
|
142
|
+
container_name: str,
|
|
143
|
+
num_lines: int,
|
|
144
|
+
previous_logs: bool = False,
|
|
145
|
+
since_timestamp: str = "",
|
|
146
|
+
include_timestamps: bool = False,
|
|
137
147
|
):
|
|
138
|
-
|
|
139
|
-
f"call SYSTEM$GET_SERVICE_LOGS('{service_name}', '{instance_id}', '{container_name}',
|
|
148
|
+
cursor = self.execute_query(
|
|
149
|
+
f"call SYSTEM$GET_SERVICE_LOGS('{service_name}', '{instance_id}', '{container_name}', "
|
|
150
|
+
f"{num_lines}, {previous_logs}, '{since_timestamp}', {include_timestamps});"
|
|
140
151
|
)
|
|
141
152
|
|
|
153
|
+
for log in cursor.fetchall():
|
|
154
|
+
yield log[0] if isinstance(log, tuple) else log
|
|
155
|
+
|
|
156
|
+
def stream_logs(
|
|
157
|
+
self,
|
|
158
|
+
service_name: str,
|
|
159
|
+
instance_id: str,
|
|
160
|
+
container_name: str,
|
|
161
|
+
num_lines: int,
|
|
162
|
+
since_timestamp: str,
|
|
163
|
+
include_timestamps: bool,
|
|
164
|
+
interval_seconds: int,
|
|
165
|
+
):
|
|
166
|
+
try:
|
|
167
|
+
prev_timestamp = since_timestamp
|
|
168
|
+
prev_log_records: List[str] = []
|
|
169
|
+
|
|
170
|
+
while True:
|
|
171
|
+
raw_log_blocks = [
|
|
172
|
+
log
|
|
173
|
+
for log in self.logs(
|
|
174
|
+
service_name=service_name,
|
|
175
|
+
instance_id=instance_id,
|
|
176
|
+
container_name=container_name,
|
|
177
|
+
num_lines=num_lines,
|
|
178
|
+
since_timestamp=prev_timestamp,
|
|
179
|
+
include_timestamps=True,
|
|
180
|
+
)
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
new_log_records = []
|
|
184
|
+
for block in raw_log_blocks:
|
|
185
|
+
new_log_records.extend(block.split("\n"))
|
|
186
|
+
|
|
187
|
+
new_log_records = [line for line in new_log_records if line.strip()]
|
|
188
|
+
|
|
189
|
+
if new_log_records:
|
|
190
|
+
dedup_log_records = new_logs_only(prev_log_records, new_log_records)
|
|
191
|
+
for log in dedup_log_records:
|
|
192
|
+
yield filter_log_timestamp(log, include_timestamps)
|
|
193
|
+
|
|
194
|
+
prev_timestamp = dedup_log_records[-1].split(" ", 1)[0]
|
|
195
|
+
prev_log_records = dedup_log_records
|
|
196
|
+
|
|
197
|
+
time.sleep(interval_seconds)
|
|
198
|
+
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
return
|
|
201
|
+
|
|
142
202
|
def upgrade_spec(self, service_name: str, spec_path: Path):
|
|
143
203
|
spec = self._read_yaml(spec_path)
|
|
144
204
|
query = f"alter service {service_name} from specification $$ {spec} $$"
|
|
145
|
-
return self.
|
|
205
|
+
return self.execute_query(query)
|
|
146
206
|
|
|
147
207
|
def list_endpoints(self, service_name: str) -> SnowflakeCursor:
|
|
148
|
-
return self.
|
|
208
|
+
return self.execute_query(f"show endpoints in service {service_name}")
|
|
209
|
+
|
|
210
|
+
def list_instances(self, service_name: str) -> SnowflakeCursor:
|
|
211
|
+
return self.execute_query(f"show service instances in service {service_name}")
|
|
212
|
+
|
|
213
|
+
def list_containers(self, service_name: str) -> SnowflakeCursor:
|
|
214
|
+
return self.execute_query(f"show service containers in service {service_name}")
|
|
215
|
+
|
|
216
|
+
def list_roles(self, service_name: str) -> SnowflakeCursor:
|
|
217
|
+
return self.execute_query(f"show roles in service {service_name}")
|
|
149
218
|
|
|
150
219
|
def suspend(self, service_name: str):
|
|
151
|
-
return self.
|
|
220
|
+
return self.execute_query(f"alter service {service_name} suspend")
|
|
152
221
|
|
|
153
222
|
def resume(self, service_name: str):
|
|
154
|
-
return self.
|
|
223
|
+
return self.execute_query(f"alter service {service_name} resume")
|
|
155
224
|
|
|
156
225
|
def set_property(
|
|
157
226
|
self,
|
|
@@ -160,6 +229,7 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
160
229
|
max_instances: Optional[int],
|
|
161
230
|
query_warehouse: Optional[str],
|
|
162
231
|
auto_resume: Optional[bool],
|
|
232
|
+
external_access_integrations: Optional[List[str]],
|
|
163
233
|
comment: Optional[str],
|
|
164
234
|
):
|
|
165
235
|
property_pairs = [
|
|
@@ -167,6 +237,7 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
167
237
|
("max_instances", max_instances),
|
|
168
238
|
("query_warehouse", query_warehouse),
|
|
169
239
|
("auto_resume", auto_resume),
|
|
240
|
+
("external_access_integrations", external_access_integrations),
|
|
170
241
|
("comment", comment),
|
|
171
242
|
]
|
|
172
243
|
|
|
@@ -175,11 +246,32 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
175
246
|
raise NoPropertiesProvidedError(
|
|
176
247
|
f"No properties specified for service '{service_name}'. Please provide at least one property to set."
|
|
177
248
|
)
|
|
178
|
-
query: List[str] = [f"alter service {service_name} set"]
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
249
|
+
query: List[str] = [f"alter service {service_name} set "]
|
|
250
|
+
|
|
251
|
+
if min_instances is not None:
|
|
252
|
+
query.append(f" min_instances = {min_instances}")
|
|
253
|
+
|
|
254
|
+
if max_instances is not None:
|
|
255
|
+
query.append(f" max_instances = {max_instances}")
|
|
256
|
+
|
|
257
|
+
if query_warehouse is not None:
|
|
258
|
+
query.append(f" query_warehouse = {query_warehouse}")
|
|
259
|
+
|
|
260
|
+
if auto_resume is not None:
|
|
261
|
+
query.append(f" auto_resume = {auto_resume}")
|
|
262
|
+
|
|
263
|
+
if external_access_integrations is not None:
|
|
264
|
+
external_access_integration_list = ",".join(
|
|
265
|
+
f"{e}" for e in external_access_integrations
|
|
266
|
+
)
|
|
267
|
+
query.append(
|
|
268
|
+
f"external_access_integrations = ({external_access_integration_list})"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if comment is not None:
|
|
272
|
+
query.append(f" comment = {comment}")
|
|
273
|
+
|
|
274
|
+
return self.execute_query(strip_empty_lines(query))
|
|
183
275
|
|
|
184
276
|
def unset_property(
|
|
185
277
|
self,
|
|
@@ -205,4 +297,4 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
205
297
|
)
|
|
206
298
|
unset_list = [property_name for property_name, value in property_pairs if value]
|
|
207
299
|
query = f"alter service {service_name} unset {','.join(unset_list)}"
|
|
208
|
-
return self.
|
|
300
|
+
return self.execute_query(query)
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import List, Optional
|
|
19
19
|
|
|
20
|
+
import typer
|
|
20
21
|
from snowflake.cli._plugins.sql.manager import SqlManager
|
|
21
22
|
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
22
23
|
from snowflake.cli.api.commands.flags import (
|
|
@@ -63,6 +64,11 @@ def execute_sql(
|
|
|
63
64
|
"String in format of key=value. If provided the SQL content will "
|
|
64
65
|
"be treated as template and rendered using provided data.",
|
|
65
66
|
),
|
|
67
|
+
retain_comments: Optional[bool] = typer.Option(
|
|
68
|
+
False,
|
|
69
|
+
"--retain-comments",
|
|
70
|
+
help="Retains comments in queries passed to Snowflake",
|
|
71
|
+
),
|
|
66
72
|
**options,
|
|
67
73
|
) -> CommandResult:
|
|
68
74
|
"""
|
|
@@ -80,7 +86,9 @@ def execute_sql(
|
|
|
80
86
|
if data_override:
|
|
81
87
|
data = {v.key: v.value for v in parse_key_value_variables(data_override)}
|
|
82
88
|
|
|
83
|
-
single_statement, cursors = SqlManager().execute(
|
|
89
|
+
single_statement, cursors = SqlManager().execute(
|
|
90
|
+
query, files, std_in, data=data, retain_comments=retain_comments
|
|
91
|
+
)
|
|
84
92
|
if single_statement:
|
|
85
93
|
return QueryResult(next(cursors))
|
|
86
94
|
return MultipleResults((QueryResult(c) for c in cursors))
|
|
@@ -39,6 +39,7 @@ class SqlManager(SqlExecutionMixin):
|
|
|
39
39
|
files: List[Path] | None,
|
|
40
40
|
std_in: bool,
|
|
41
41
|
data: Dict | None = None,
|
|
42
|
+
retain_comments: bool = False,
|
|
42
43
|
) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
|
|
43
44
|
inputs = [query, files, std_in]
|
|
44
45
|
# Check if any two inputs were provided simultaneously
|
|
@@ -50,7 +51,9 @@ class SqlManager(SqlExecutionMixin):
|
|
|
50
51
|
if std_in:
|
|
51
52
|
query = sys.stdin.read()
|
|
52
53
|
if query:
|
|
53
|
-
return self._execute_single_query(
|
|
54
|
+
return self._execute_single_query(
|
|
55
|
+
query=query, data=data, retain_comments=retain_comments
|
|
56
|
+
)
|
|
54
57
|
|
|
55
58
|
if files:
|
|
56
59
|
# Multiple files
|
|
@@ -61,7 +64,7 @@ class SqlManager(SqlExecutionMixin):
|
|
|
61
64
|
file_size_limit_mb=UNLIMITED
|
|
62
65
|
)
|
|
63
66
|
single_statement, result = self._execute_single_query(
|
|
64
|
-
query=query_from_file, data=data
|
|
67
|
+
query=query_from_file, data=data, retain_comments=retain_comments
|
|
65
68
|
)
|
|
66
69
|
results.append(result)
|
|
67
70
|
|
|
@@ -73,7 +76,7 @@ class SqlManager(SqlExecutionMixin):
|
|
|
73
76
|
raise UsageError("Use either query, filename or input option.")
|
|
74
77
|
|
|
75
78
|
def _execute_single_query(
|
|
76
|
-
self, query: str, data: Dict | None = None
|
|
79
|
+
self, query: str, data: Dict | None = None, retain_comments: bool = False
|
|
77
80
|
) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
|
|
78
81
|
try:
|
|
79
82
|
query = transpile_snowsql_templates(query)
|
|
@@ -83,7 +86,9 @@ class SqlManager(SqlExecutionMixin):
|
|
|
83
86
|
|
|
84
87
|
statements = tuple(
|
|
85
88
|
statement
|
|
86
|
-
for statement, _ in split_statements(
|
|
89
|
+
for statement, _ in split_statements(
|
|
90
|
+
StringIO(query), remove_comments=not retain_comments
|
|
91
|
+
)
|
|
87
92
|
)
|
|
88
93
|
single_statement = len(statements) == 1
|
|
89
94
|
|