snowflake-cli 3.1.0__py3-none-any.whl → 3.2.1__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/dev/docs/templates/usage.rst.jinja2 +1 -1
- snowflake/cli/_plugins/connection/commands.py +124 -109
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/manager.py +4 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +10 -3
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -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/compat.py +1 -89
- snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
- 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 +3 -3
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
- 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/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +64 -13
- snowflake/cli/_plugins/spcs/services/manager.py +75 -15
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +20 -16
- snowflake/cli/_plugins/stage/diff.py +1 -1
- snowflake/cli/_plugins/stage/manager.py +140 -11
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +6 -3
- snowflake/cli/api/cli_global_context.py +1 -0
- snowflake/cli/api/config.py +23 -5
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/utils.py +19 -32
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +9 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +179 -62
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +35 -22
- snowflake/cli/api/stage_path.py +5 -2
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/METADATA +7 -8
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/RECORD +56 -55
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -392
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,6 +21,8 @@ from click import ClickException
|
|
|
21
21
|
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
22
22
|
from snowflake.cli.api.commands.flags import (
|
|
23
23
|
IdentifierType,
|
|
24
|
+
IfNotExistsOption,
|
|
25
|
+
ReplaceOption,
|
|
24
26
|
like_option,
|
|
25
27
|
)
|
|
26
28
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
@@ -145,6 +147,8 @@ def create(
|
|
|
145
147
|
object_type: str = ObjectArgument,
|
|
146
148
|
object_attributes: Optional[List[str]] = ObjectAttributesArgument,
|
|
147
149
|
object_json: str = ObjectDefinitionJsonOption,
|
|
150
|
+
if_not_exists: bool = IfNotExistsOption(),
|
|
151
|
+
replace: bool = ReplaceOption(),
|
|
148
152
|
**options,
|
|
149
153
|
):
|
|
150
154
|
"""
|
|
@@ -176,5 +180,10 @@ def create(
|
|
|
176
180
|
"Provide either list of object attributes, or object definition in JSON format"
|
|
177
181
|
)
|
|
178
182
|
|
|
179
|
-
result = ObjectManager().create(
|
|
183
|
+
result = ObjectManager().create(
|
|
184
|
+
object_type=object_type,
|
|
185
|
+
object_data=object_data,
|
|
186
|
+
if_not_exists=if_not_exists,
|
|
187
|
+
replace=replace,
|
|
188
|
+
)
|
|
180
189
|
return MessageResult(result)
|
|
@@ -52,11 +52,11 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
52
52
|
query += f" like '{like}'"
|
|
53
53
|
if scope[0] is not None:
|
|
54
54
|
query += f" in {scope[0].replace('-', ' ')} {scope[1]}"
|
|
55
|
-
return self.
|
|
55
|
+
return self.execute_query(query, **kwargs)
|
|
56
56
|
|
|
57
57
|
def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor:
|
|
58
58
|
object_name = _get_object_names(object_type).sf_name
|
|
59
|
-
return self.
|
|
59
|
+
return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
|
|
60
60
|
|
|
61
61
|
def describe(self, *, object_type: str, fqn: FQN):
|
|
62
62
|
# Image repository is the only supported object that does not have a DESCRIBE command.
|
|
@@ -65,7 +65,7 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
65
65
|
f"Describe is currently not supported for object of type image-repository"
|
|
66
66
|
)
|
|
67
67
|
object_name = _get_object_names(object_type).sf_name
|
|
68
|
-
return self.
|
|
68
|
+
return self.execute_query(f"describe {object_name} {fqn.sql_identifier}")
|
|
69
69
|
|
|
70
70
|
def object_exists(self, *, object_type: str, fqn: FQN):
|
|
71
71
|
try:
|
|
@@ -74,9 +74,17 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
74
74
|
except ProgrammingError:
|
|
75
75
|
return False
|
|
76
76
|
|
|
77
|
-
def create(
|
|
77
|
+
def create(
|
|
78
|
+
self,
|
|
79
|
+
object_type: str,
|
|
80
|
+
object_data: Dict[str, Any],
|
|
81
|
+
replace: bool = False,
|
|
82
|
+
if_not_exists: bool = False,
|
|
83
|
+
) -> str:
|
|
78
84
|
rest = RestApi(self._conn)
|
|
79
|
-
url = rest.determine_url_for_create_query(
|
|
85
|
+
url = rest.determine_url_for_create_query(
|
|
86
|
+
object_type=object_type, replace=replace, if_not_exists=if_not_exists
|
|
87
|
+
)
|
|
80
88
|
try:
|
|
81
89
|
response = rest.send_rest_request(url=url, method="post", data=object_data)
|
|
82
90
|
except Exception as err:
|
|
@@ -58,9 +58,9 @@ class SnowparkObjectManager(SqlExecutionMixin):
|
|
|
58
58
|
self, execution_identifier: str, object_type: SnowparkObject
|
|
59
59
|
) -> SnowflakeCursor:
|
|
60
60
|
if object_type == SnowparkObject.FUNCTION:
|
|
61
|
-
return self.
|
|
61
|
+
return self.execute_query(f"select {execution_identifier}")
|
|
62
62
|
if object_type == SnowparkObject.PROCEDURE:
|
|
63
|
-
return self.
|
|
63
|
+
return self.execute_query(f"call {execution_identifier}")
|
|
64
64
|
raise UsageError(f"Unknown object type: {object_type}.")
|
|
65
65
|
|
|
66
66
|
def create_or_replace(
|
|
@@ -95,7 +95,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
|
|
|
95
95
|
if isinstance(entity, ProcedureEntityModel) and entity.execute_as_caller:
|
|
96
96
|
query.append("execute as caller")
|
|
97
97
|
|
|
98
|
-
return self.
|
|
98
|
+
return self.execute_query("\n".join(query))
|
|
99
99
|
|
|
100
100
|
def deploy_entity(
|
|
101
101
|
self,
|
|
@@ -177,7 +177,7 @@ class AnacondaPackagesManager(SqlExecutionMixin):
|
|
|
177
177
|
return AnacondaPackages(packages)
|
|
178
178
|
|
|
179
179
|
def _query_snowflake_for_available_packages(self) -> dict[str, AvailablePackage]:
|
|
180
|
-
cursor = self.
|
|
180
|
+
cursor = self.execute_query(
|
|
181
181
|
"select package_name, version from snowflake.information_schema.packages where language = 'python'",
|
|
182
182
|
cursor_class=DictCursor,
|
|
183
183
|
)
|
|
@@ -95,5 +95,34 @@ def handle_object_already_exists(
|
|
|
95
95
|
raise error
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def filter_log_timestamp(log: str, include_timestamps: bool) -> str:
|
|
99
|
+
if include_timestamps:
|
|
100
|
+
return log
|
|
101
|
+
else:
|
|
102
|
+
return log.split(" ", 1)[1] if " " in log else log
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def new_logs_only(prev_log_records: list[str], new_log_records: list[str]) -> list[str]:
|
|
106
|
+
# Sort the log records, we get time-ordered logs
|
|
107
|
+
# due to ISO 8601 timestamp format in the log content
|
|
108
|
+
# eg: 2024-10-22T01:12:29.873896187Z Count: 1
|
|
109
|
+
new_log_records_sorted = sorted(new_log_records)
|
|
110
|
+
|
|
111
|
+
# Get the first new log record to establish the overlap point
|
|
112
|
+
first_new_log_record = new_log_records_sorted[0]
|
|
113
|
+
|
|
114
|
+
# Traverse previous logs in reverse and remove duplicates from new logs
|
|
115
|
+
for prev_log in reversed(prev_log_records):
|
|
116
|
+
# Stop if the previous log is earlier than the first new log
|
|
117
|
+
if prev_log < first_new_log_record:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# Remove matching previous logs from the new logs list
|
|
121
|
+
if prev_log in new_log_records_sorted:
|
|
122
|
+
new_log_records_sorted.remove(prev_log)
|
|
123
|
+
|
|
124
|
+
return new_log_records_sorted
|
|
125
|
+
|
|
126
|
+
|
|
98
127
|
class NoPropertiesProvidedError(ClickException):
|
|
99
128
|
pass
|
|
@@ -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()
|
|
@@ -84,4 +84,4 @@ class ImageRepositoryManager(SqlExecutionMixin):
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
def list_images(self, repo_name: str) -> SnowflakeCursor:
|
|
87
|
-
return self.
|
|
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(
|
|
@@ -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)
|
|
@@ -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,37 +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}")
|
|
149
209
|
|
|
150
210
|
def list_instances(self, service_name: str) -> SnowflakeCursor:
|
|
151
|
-
return self.
|
|
211
|
+
return self.execute_query(f"show service instances in service {service_name}")
|
|
152
212
|
|
|
153
213
|
def list_containers(self, service_name: str) -> SnowflakeCursor:
|
|
154
|
-
return self.
|
|
214
|
+
return self.execute_query(f"show service containers in service {service_name}")
|
|
155
215
|
|
|
156
216
|
def list_roles(self, service_name: str) -> SnowflakeCursor:
|
|
157
|
-
return self.
|
|
217
|
+
return self.execute_query(f"show roles in service {service_name}")
|
|
158
218
|
|
|
159
219
|
def suspend(self, service_name: str):
|
|
160
|
-
return self.
|
|
220
|
+
return self.execute_query(f"alter service {service_name} suspend")
|
|
161
221
|
|
|
162
222
|
def resume(self, service_name: str):
|
|
163
|
-
return self.
|
|
223
|
+
return self.execute_query(f"alter service {service_name} resume")
|
|
164
224
|
|
|
165
225
|
def set_property(
|
|
166
226
|
self,
|
|
@@ -211,7 +271,7 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
211
271
|
if comment is not None:
|
|
212
272
|
query.append(f" comment = {comment}")
|
|
213
273
|
|
|
214
|
-
return self.
|
|
274
|
+
return self.execute_query(strip_empty_lines(query))
|
|
215
275
|
|
|
216
276
|
def unset_property(
|
|
217
277
|
self,
|
|
@@ -237,4 +297,4 @@ class ServiceManager(SqlExecutionMixin):
|
|
|
237
297
|
)
|
|
238
298
|
unset_list = [property_name for property_name, value in property_pairs if value]
|
|
239
299
|
query = f"alter service {service_name} unset {','.join(unset_list)}"
|
|
240
|
-
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
|
|
|
@@ -141,7 +141,7 @@ def copy(
|
|
|
141
141
|
)
|
|
142
142
|
return _put(
|
|
143
143
|
recursive=recursive,
|
|
144
|
-
source_path=source_path,
|
|
144
|
+
source_path=Path(source_path),
|
|
145
145
|
destination_path=destination_path,
|
|
146
146
|
parallel=parallel,
|
|
147
147
|
overwrite=overwrite,
|
|
@@ -247,23 +247,27 @@ def get(recursive: bool, source_path: str, destination_path: str, parallel: int)
|
|
|
247
247
|
|
|
248
248
|
def _put(
|
|
249
249
|
recursive: bool,
|
|
250
|
-
source_path:
|
|
250
|
+
source_path: Path,
|
|
251
251
|
destination_path: str,
|
|
252
252
|
parallel: int,
|
|
253
253
|
overwrite: bool,
|
|
254
254
|
auto_compress: bool,
|
|
255
255
|
):
|
|
256
|
-
if recursive:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
256
|
+
if recursive and not source_path.is_file():
|
|
257
|
+
cursor_generator = StageManager().put_recursive(
|
|
258
|
+
local_path=source_path,
|
|
259
|
+
stage_path=destination_path,
|
|
260
|
+
overwrite=overwrite,
|
|
261
|
+
parallel=parallel,
|
|
262
|
+
auto_compress=auto_compress,
|
|
263
|
+
)
|
|
264
|
+
return CollectionResult(cursor_generator)
|
|
265
|
+
else:
|
|
266
|
+
cursor = StageManager().put(
|
|
267
|
+
local_path=source_path.resolve(),
|
|
268
|
+
stage_path=destination_path,
|
|
269
|
+
overwrite=overwrite,
|
|
270
|
+
parallel=parallel,
|
|
271
|
+
auto_compress=auto_compress,
|
|
272
|
+
)
|
|
273
|
+
return QueryResult(cursor)
|
|
@@ -221,7 +221,7 @@ def put_files_on_stage(
|
|
|
221
221
|
|
|
222
222
|
|
|
223
223
|
def sync_local_diff_with_stage(
|
|
224
|
-
role: str, deploy_root_path: Path, diff_result: DiffResult, stage_fqn: str
|
|
224
|
+
role: str | None, deploy_root_path: Path, diff_result: DiffResult, stage_fqn: str
|
|
225
225
|
):
|
|
226
226
|
"""
|
|
227
227
|
Syncs a given local directory's contents with a Snowflake stage, including removing old files, and re-uploading modified and new files.
|