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.
Files changed (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +152 -99
  7. snowflake/cli/_plugins/connection/util.py +54 -9
  8. snowflake/cli/_plugins/cortex/manager.py +1 -1
  9. snowflake/cli/_plugins/git/commands.py +6 -3
  10. snowflake/cli/_plugins/git/manager.py +9 -4
  11. snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  14. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  15. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  19. snowflake/cli/_plugins/nativeapp/commands.py +144 -188
  20. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
  23. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  24. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  25. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  26. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  27. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  28. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  29. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
  32. snowflake/cli/_plugins/notebook/manager.py +2 -2
  33. snowflake/cli/_plugins/object/commands.py +10 -1
  34. snowflake/cli/_plugins/object/manager.py +13 -5
  35. snowflake/cli/_plugins/snowpark/common.py +63 -21
  36. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
  37. snowflake/cli/_plugins/spcs/common.py +29 -0
  38. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  39. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  40. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  41. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  42. snowflake/cli/_plugins/spcs/services/commands.py +100 -17
  43. snowflake/cli/_plugins/spcs/services/manager.py +108 -16
  44. snowflake/cli/_plugins/sql/commands.py +9 -1
  45. snowflake/cli/_plugins/sql/manager.py +9 -4
  46. snowflake/cli/_plugins/stage/commands.py +28 -19
  47. snowflake/cli/_plugins/stage/diff.py +17 -17
  48. snowflake/cli/_plugins/stage/manager.py +304 -84
  49. snowflake/cli/_plugins/stage/md5.py +1 -1
  50. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  51. snowflake/cli/_plugins/workspace/commands.py +27 -4
  52. snowflake/cli/_plugins/workspace/context.py +38 -0
  53. snowflake/cli/_plugins/workspace/manager.py +23 -13
  54. snowflake/cli/api/cli_global_context.py +4 -3
  55. snowflake/cli/api/commands/flags.py +23 -7
  56. snowflake/cli/api/config.py +30 -9
  57. snowflake/cli/api/connections.py +12 -1
  58. snowflake/cli/api/console/console.py +4 -19
  59. snowflake/cli/api/entities/common.py +4 -2
  60. snowflake/cli/api/entities/utils.py +36 -69
  61. snowflake/cli/api/errno.py +2 -0
  62. snowflake/cli/api/exceptions.py +41 -0
  63. snowflake/cli/api/identifiers.py +8 -0
  64. snowflake/cli/api/metrics.py +223 -7
  65. snowflake/cli/api/output/types.py +1 -1
  66. snowflake/cli/api/project/definition_conversion.py +293 -77
  67. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  68. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  69. snowflake/cli/api/rest_api.py +26 -4
  70. snowflake/cli/api/secure_utils.py +1 -1
  71. snowflake/cli/api/sql_execution.py +40 -29
  72. snowflake/cli/api/stage_path.py +244 -0
  73. snowflake/cli/api/utils/definition_rendering.py +3 -5
  74. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
  75. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
  76. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  77. snowflake/cli/_plugins/nativeapp/manager.py +0 -415
  78. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  79. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  80. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  81. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
  82. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  83. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  84. {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._execute_query(strip_empty_lines(query))
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._execute_query(f"alter compute pool {pool_name} stop all")
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._execute_query(f"alter compute pool {pool_name} suspend")
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._execute_query(f"alter compute pool {pool_name} resume")
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._execute_query(strip_empty_lines(query))
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._execute_query(query)
116
+ return self.execute_query(query)
117
117
 
118
118
  def status(self, pool_name: str):
119
- return self._execute_query(
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._execute_query(
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._execute_query(repositories_query, cursor_class=DictCursor)
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
- repository_manager = ImageRepositoryManager()
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 sys
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 External Access Integrations(EAI) that the service can access. This option may be specified multiple times for multiple EAIs.",
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 External Access Integrations(EAI) that the job service can access. This option may be specified multiple times for multiple EAIs.",
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
- 500, "--num-lines", help="Number of lines to retrieve."
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
- results = ServiceManager().logs(
230
- service_name=name.identifier,
231
- instance_id=instance_id,
232
- container_name=container_name,
233
- num_lines=num_lines,
234
- )
235
- cursor = results.fetchone()
236
- logs = next(iter(cursor)).split("\n")
237
- print_log_lines(sys.stdout, name, "0", logs)
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._execute_query(strip_empty_lines(query))
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._execute_query(strip_empty_lines(query))
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._execute_query(f"CALL SYSTEM$GET_SERVICE_STATUS('{service_name}')")
136
+ return self.execute_query(f"CALL SYSTEM$GET_SERVICE_STATUS('{service_name}')")
134
137
 
135
138
  def logs(
136
- self, service_name: str, instance_id: str, container_name: str, num_lines: int
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
- return self._execute_query(
139
- f"call SYSTEM$GET_SERVICE_LOGS('{service_name}', '{instance_id}', '{container_name}', {num_lines});"
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._execute_query(query)
205
+ return self.execute_query(query)
146
206
 
147
207
  def list_endpoints(self, service_name: str) -> SnowflakeCursor:
148
- return self._execute_query(f"show endpoints in service {service_name}")
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._execute_query(f"alter service {service_name} suspend")
220
+ return self.execute_query(f"alter service {service_name} suspend")
152
221
 
153
222
  def resume(self, service_name: str):
154
- return self._execute_query(f"alter service {service_name} resume")
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
- for property_name, value in property_pairs:
180
- if value is not None:
181
- query.append(f"{property_name} = {value}")
182
- return self._execute_query(strip_empty_lines(query))
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._execute_query(query)
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(query, files, std_in, data=data)
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(query=query, data=data)
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(StringIO(query), remove_comments=True)
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