snowflake-cli 3.2.1__py3-none-any.whl → 3.3.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 (56) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/constants.py +4 -0
  3. snowflake/cli/_app/snow_connector.py +12 -0
  4. snowflake/cli/_app/telemetry.py +10 -3
  5. snowflake/cli/_plugins/connection/util.py +12 -19
  6. snowflake/cli/_plugins/helpers/commands.py +207 -1
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
  8. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
  9. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
  10. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
  11. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
  12. snowflake/cli/_plugins/nativeapp/commands.py +92 -2
  13. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  14. snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
  15. snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
  16. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  17. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  18. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  19. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
  20. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  21. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
  22. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  23. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  24. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
  25. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  26. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
  27. snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
  28. snowflake/cli/_plugins/notebook/manager.py +4 -2
  29. snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
  30. snowflake/cli/_plugins/spcs/common.py +129 -0
  31. snowflake/cli/_plugins/spcs/services/commands.py +134 -14
  32. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  33. snowflake/cli/_plugins/stage/manager.py +12 -4
  34. snowflake/cli/_plugins/streamlit/manager.py +8 -1
  35. snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
  36. snowflake/cli/_plugins/workspace/commands.py +3 -2
  37. snowflake/cli/_plugins/workspace/manager.py +8 -4
  38. snowflake/cli/api/cli_global_context.py +22 -1
  39. snowflake/cli/api/config.py +6 -2
  40. snowflake/cli/api/connections.py +12 -1
  41. snowflake/cli/api/constants.py +9 -1
  42. snowflake/cli/api/entities/common.py +85 -0
  43. snowflake/cli/api/entities/utils.py +9 -8
  44. snowflake/cli/api/errno.py +60 -3
  45. snowflake/cli/api/feature_flags.py +20 -4
  46. snowflake/cli/api/metrics.py +21 -27
  47. snowflake/cli/api/project/definition_conversion.py +1 -2
  48. snowflake/cli/api/project/schemas/project_definition.py +27 -6
  49. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  50. snowflake/cli/api/project/util.py +45 -0
  51. snowflake/cli/api/rest_api.py +3 -2
  52. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
  53. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
  54. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
  55. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
  56. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -37,9 +37,13 @@ from snowflake.cli.api.commands.flags import (
37
37
  )
38
38
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
39
39
  from snowflake.cli.api.constants import ObjectType
40
- from snowflake.cli.api.exceptions import IncompatibleParametersError
40
+ from snowflake.cli.api.exceptions import (
41
+ IncompatibleParametersError,
42
+ )
43
+ from snowflake.cli.api.feature_flags import FeatureFlag
41
44
  from snowflake.cli.api.identifiers import FQN
42
45
  from snowflake.cli.api.output.types import (
46
+ CollectionResult,
43
47
  CommandResult,
44
48
  MessageResult,
45
49
  QueryJsonValueResult,
@@ -55,6 +59,38 @@ app = SnowTyperFactory(
55
59
  short_help="Manages services.",
56
60
  )
57
61
 
62
+ # Define common options
63
+ container_name_option = typer.Option(
64
+ ...,
65
+ "--container-name",
66
+ help="Name of the container.",
67
+ show_default=False,
68
+ )
69
+
70
+ instance_id_option = typer.Option(
71
+ ...,
72
+ "--instance-id",
73
+ help="ID of the service instance, starting with 0.",
74
+ show_default=False,
75
+ )
76
+
77
+ since_option = typer.Option(
78
+ default="",
79
+ help="Fetch events that are newer than this time ago, in Snowflake interval syntax.",
80
+ )
81
+
82
+ until_option = typer.Option(
83
+ default="",
84
+ help="Fetch events that are older than this time ago, in Snowflake interval syntax.",
85
+ )
86
+
87
+ show_all_columns_option = typer.Option(
88
+ False,
89
+ "--all",
90
+ is_flag=True,
91
+ help="Fetch all columns.",
92
+ )
93
+
58
94
 
59
95
  def _service_name_callback(name: FQN) -> FQN:
60
96
  if not is_valid_object_name(name.identifier, max_depth=2, allow_quoted=False):
@@ -209,18 +245,8 @@ def status(name: FQN = ServiceNameArgument, **options) -> CommandResult:
209
245
  @app.command(requires_connection=True)
210
246
  def logs(
211
247
  name: FQN = ServiceNameArgument,
212
- container_name: str = typer.Option(
213
- ...,
214
- "--container-name",
215
- help="Name of the container.",
216
- show_default=False,
217
- ),
218
- instance_id: str = typer.Option(
219
- ...,
220
- "--instance-id",
221
- help="ID of the service instance, starting with 0.",
222
- show_default=False,
223
- ),
248
+ container_name: str = container_name_option,
249
+ instance_id: str = instance_id_option,
224
250
  num_lines: int = typer.Option(
225
251
  DEFAULT_NUM_LINES, "--num-lines", help="Number of lines to retrieve."
226
252
  ),
@@ -237,12 +263,17 @@ def logs(
237
263
  False, "--include-timestamps", help="Include timestamps in logs.", is_flag=True
238
264
  ),
239
265
  follow: bool = typer.Option(
240
- False, "--follow", help="Stream logs in real-time.", is_flag=True
266
+ False,
267
+ "--follow",
268
+ help="Stream logs in real-time.",
269
+ is_flag=True,
270
+ hidden=True,
241
271
  ),
242
272
  follow_interval: int = typer.Option(
243
273
  2,
244
274
  "--follow-interval",
245
275
  help="Set custom polling intervals for log streaming (--follow flag) in seconds.",
276
+ hidden=True,
246
277
  ),
247
278
  **options,
248
279
  ):
@@ -288,6 +319,95 @@ def logs(
288
319
  return StreamResult(cast(Generator[CommandResult, None, None], stream))
289
320
 
290
321
 
322
+ @app.command(
323
+ requires_connection=True,
324
+ is_enabled=FeatureFlag.ENABLE_SPCS_SERVICE_EVENTS.is_enabled,
325
+ )
326
+ def events(
327
+ name: FQN = ServiceNameArgument,
328
+ container_name: str = container_name_option,
329
+ instance_id: str = instance_id_option,
330
+ since: str = since_option,
331
+ until: str = until_option,
332
+ first: int = typer.Option(
333
+ default=None,
334
+ show_default=False,
335
+ help="Fetch only the first N events. Cannot be used with --last.",
336
+ ),
337
+ last: int = typer.Option(
338
+ default=None,
339
+ show_default=False,
340
+ help="Fetch only the last N events. Cannot be used with --first.",
341
+ ),
342
+ show_all_columns: bool = show_all_columns_option,
343
+ **options,
344
+ ):
345
+ """
346
+ Retrieve platform events for a service container.
347
+ """
348
+
349
+ if first is not None and last is not None:
350
+ raise IncompatibleParametersError(["--first", "--last"])
351
+
352
+ manager = ServiceManager()
353
+ events = manager.get_events(
354
+ service_name=name.identifier,
355
+ container_name=container_name,
356
+ instance_id=instance_id,
357
+ since=since,
358
+ until=until,
359
+ first=first,
360
+ last=last,
361
+ show_all_columns=show_all_columns,
362
+ )
363
+
364
+ if not events:
365
+ return MessageResult("No events found.")
366
+
367
+ return CollectionResult(events)
368
+
369
+
370
+ @app.command(
371
+ requires_connection=True,
372
+ is_enabled=FeatureFlag.ENABLE_SPCS_SERVICE_METRICS.is_enabled,
373
+ )
374
+ def metrics(
375
+ name: FQN = ServiceNameArgument,
376
+ container_name: str = container_name_option,
377
+ instance_id: str = instance_id_option,
378
+ since: str = since_option,
379
+ until: str = until_option,
380
+ show_all_columns: bool = show_all_columns_option,
381
+ **options,
382
+ ):
383
+ """
384
+ Retrieve platform metrics for a service container.
385
+ """
386
+
387
+ manager = ServiceManager()
388
+ if since or until:
389
+ metrics = manager.get_all_metrics(
390
+ service_name=name.identifier,
391
+ container_name=container_name,
392
+ instance_id=instance_id,
393
+ since=since,
394
+ until=until,
395
+ show_all_columns=show_all_columns,
396
+ )
397
+ else:
398
+ metrics = manager.get_latest_metrics(
399
+ service_name=name.identifier,
400
+ container_name=container_name,
401
+ instance_id=instance_id,
402
+ show_all_columns=show_all_columns,
403
+ )
404
+
405
+ if not metrics:
406
+ return MessageResult("No metrics found.")
407
+
408
+ return CollectionResult(metrics)
409
+
410
+
291
411
  @app.command(requires_connection=True)
292
412
  def upgrade(
293
413
  name: FQN = ServiceNameArgument,
@@ -16,14 +16,21 @@ from __future__ import annotations
16
16
 
17
17
  import json
18
18
  import time
19
+ from datetime import datetime
19
20
  from pathlib import Path
20
21
  from typing import List, Optional
21
22
 
22
23
  import yaml
23
24
  from snowflake.cli._plugins.object.common import Tag
24
25
  from snowflake.cli._plugins.spcs.common import (
26
+ EVENT_COLUMN_NAMES,
25
27
  NoPropertiesProvidedError,
28
+ SPCSEventTableError,
29
+ build_resource_clause,
30
+ build_time_clauses,
26
31
  filter_log_timestamp,
32
+ format_event_row,
33
+ format_metric_row,
27
34
  handle_object_already_exists,
28
35
  new_logs_only,
29
36
  strip_empty_lines,
@@ -31,7 +38,7 @@ from snowflake.cli._plugins.spcs.common import (
31
38
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
32
39
  from snowflake.cli.api.secure_path import SecurePath
33
40
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
34
- from snowflake.connector.cursor import SnowflakeCursor
41
+ from snowflake.connector.cursor import DictCursor, SnowflakeCursor
35
42
  from snowflake.connector.errors import ProgrammingError
36
43
 
37
44
 
@@ -199,6 +206,167 @@ class ServiceManager(SqlExecutionMixin):
199
206
  except KeyboardInterrupt:
200
207
  return
201
208
 
209
+ def get_account_event_table(self):
210
+ query = "show parameters like 'event_table' in account"
211
+ results = self.execute_query(query, cursor_class=DictCursor)
212
+ event_table = next(
213
+ (r["value"] for r in results if r["key"] == "EVENT_TABLE"), ""
214
+ )
215
+ if not event_table:
216
+ raise SPCSEventTableError("No SPCS event table configured in the account.")
217
+ return event_table
218
+
219
+ def get_events(
220
+ self,
221
+ service_name: str,
222
+ instance_id: str,
223
+ container_name: str,
224
+ since: str | datetime | None = None,
225
+ until: str | datetime | None = None,
226
+ first: Optional[int] = None,
227
+ last: Optional[int] = None,
228
+ show_all_columns: bool = False,
229
+ ):
230
+
231
+ account_event_table = self.get_account_event_table()
232
+ resource_clause = build_resource_clause(
233
+ service_name, instance_id, container_name
234
+ )
235
+ since_clause, until_clause = build_time_clauses(since, until)
236
+
237
+ first_clause = f"limit {first}" if first is not None else ""
238
+ last_clause = f"limit {last}" if last is not None else ""
239
+
240
+ query = f"""\
241
+ select *
242
+ from (
243
+ select *
244
+ from {account_event_table}
245
+ where (
246
+ {resource_clause}
247
+ {since_clause}
248
+ {until_clause}
249
+ )
250
+ and record_type = 'LOG'
251
+ and scope['name'] = 'snow.spcs.platform'
252
+ order by timestamp desc
253
+ {last_clause}
254
+ )
255
+ order by timestamp asc
256
+ {first_clause}
257
+ """
258
+
259
+ cursor = self.execute_query(query)
260
+ raw_events = cursor.fetchall()
261
+ if not raw_events:
262
+ return []
263
+
264
+ if show_all_columns:
265
+ return [dict(zip(EVENT_COLUMN_NAMES, event)) for event in raw_events]
266
+
267
+ formatted_events = []
268
+ for raw_event in raw_events:
269
+ event_dict = dict(zip(EVENT_COLUMN_NAMES, raw_event))
270
+ formatted = format_event_row(event_dict)
271
+ formatted_events.append(formatted)
272
+
273
+ return formatted_events
274
+
275
+ def get_all_metrics(
276
+ self,
277
+ service_name: str,
278
+ instance_id: str,
279
+ container_name: str,
280
+ since: str | datetime | None = None,
281
+ until: str | datetime | None = None,
282
+ show_all_columns: bool = False,
283
+ ):
284
+
285
+ account_event_table = self.get_account_event_table()
286
+ resource_clause = build_resource_clause(
287
+ service_name, instance_id, container_name
288
+ )
289
+ since_clause, until_clause = build_time_clauses(since, until)
290
+
291
+ query = f"""\
292
+ select *
293
+ from {account_event_table}
294
+ where (
295
+ {resource_clause}
296
+ {since_clause}
297
+ {until_clause}
298
+ )
299
+ and record_type = 'METRIC'
300
+ and scope['name'] = 'snow.spcs.platform'
301
+ order by timestamp desc
302
+ """
303
+
304
+ cursor = self.execute_query(query)
305
+ raw_metrics = cursor.fetchall()
306
+ if not raw_metrics:
307
+ return []
308
+
309
+ if show_all_columns:
310
+ return [dict(zip(EVENT_COLUMN_NAMES, metric)) for metric in raw_metrics]
311
+
312
+ formatted_metrics = []
313
+ for raw_metric in raw_metrics:
314
+ metric_dict = dict(zip(EVENT_COLUMN_NAMES, raw_metric))
315
+ formatted = format_metric_row(metric_dict)
316
+ formatted_metrics.append(formatted)
317
+
318
+ return formatted_metrics
319
+
320
+ def get_latest_metrics(
321
+ self,
322
+ service_name: str,
323
+ instance_id: str,
324
+ container_name: str,
325
+ show_all_columns: bool = False,
326
+ ):
327
+
328
+ account_event_table = self.get_account_event_table()
329
+ resource_clause = build_resource_clause(
330
+ service_name, instance_id, container_name
331
+ )
332
+
333
+ query = f"""
334
+ with rankedmetrics as (
335
+ select
336
+ *,
337
+ row_number() over (
338
+ partition by record['metric']['name']
339
+ order by timestamp desc
340
+ ) as rank
341
+ from {account_event_table}
342
+ where
343
+ record_type = 'METRIC'
344
+ and scope['name'] = 'snow.spcs.platform'
345
+ and {resource_clause}
346
+ and timestamp > dateadd('hour', -1, current_timestamp)
347
+ )
348
+ select *
349
+ from rankedmetrics
350
+ where rank = 1
351
+ order by timestamp desc;
352
+ """
353
+
354
+ cursor = self.execute_query(query)
355
+ raw_metrics = cursor.fetchall()
356
+ if not raw_metrics:
357
+ return []
358
+
359
+ if show_all_columns:
360
+ return [dict(zip(EVENT_COLUMN_NAMES, metric)) for metric in raw_metrics]
361
+
362
+ formatted_metrics = []
363
+ for raw_metric in raw_metrics:
364
+ metric_dict = dict(zip(EVENT_COLUMN_NAMES, raw_metric))
365
+ formatted = format_metric_row(metric_dict)
366
+ formatted_metrics.append(formatted)
367
+
368
+ return formatted_metrics
369
+
202
370
  def upgrade_spec(self, service_name: str, spec_path: Path):
203
371
  spec = self._read_yaml(spec_path)
204
372
  query = f"alter service {service_name} from specification $$ {spec} $$"
@@ -41,7 +41,7 @@ from snowflake.cli.api.commands.utils import parse_key_value_variables
41
41
  from snowflake.cli.api.console import cli_console
42
42
  from snowflake.cli.api.constants import PYTHON_3_12
43
43
  from snowflake.cli.api.identifiers import FQN
44
- from snowflake.cli.api.project.util import to_string_literal
44
+ from snowflake.cli.api.project.util import extract_schema, to_string_literal
45
45
  from snowflake.cli.api.secure_path import SecurePath
46
46
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
47
47
  from snowflake.cli.api.stage_path import StagePath
@@ -86,6 +86,10 @@ class StagePathParts:
86
86
  def full_path(self) -> str:
87
87
  raise NotImplementedError
88
88
 
89
+ @property
90
+ def schema(self) -> str | None:
91
+ raise NotImplementedError
92
+
89
93
  def replace_stage_prefix(self, file_path: str) -> str:
90
94
  raise NotImplementedError
91
95
 
@@ -139,11 +143,15 @@ class DefaultStagePathParts(StagePathParts):
139
143
 
140
144
  @property
141
145
  def path(self) -> str:
142
- return f"{self.stage_name.rstrip('/')}/{self.directory}"
146
+ return f"{self.stage_name.rstrip('/')}/{self.directory}".rstrip("/")
143
147
 
144
148
  @property
145
149
  def full_path(self) -> str:
146
- return f"{self.stage.rstrip('/')}/{self.directory}"
150
+ return f"{self.stage.rstrip('/')}/{self.directory}".rstrip("/")
151
+
152
+ @property
153
+ def schema(self) -> str | None:
154
+ return extract_schema(self.stage)
147
155
 
148
156
  def replace_stage_prefix(self, file_path: str) -> str:
149
157
  stage = Path(self.stage).parts[0]
@@ -193,7 +201,7 @@ class UserStagePathParts(StagePathParts):
193
201
 
194
202
  @property
195
203
  def full_path(self) -> str:
196
- return f"{self.stage}/{self.directory}"
204
+ return f"{self.stage}/{self.directory}".rstrip("/")
197
205
 
198
206
  def replace_stage_prefix(self, file_path: str) -> str:
199
207
  if Path(file_path).parts[0] == self.stage_name:
@@ -104,8 +104,15 @@ class StreamlitManager(SqlExecutionMixin):
104
104
  query.append(f"MAIN_FILE = '{streamlit.main_file}'")
105
105
  if streamlit.imports:
106
106
  query.append(streamlit.get_imports_sql())
107
- if streamlit.query_warehouse:
107
+
108
+ if not streamlit.query_warehouse:
109
+ cli_console.warning(
110
+ "[Deprecation] In next major version we will remove default query_warehouse='streamlit'."
111
+ )
112
+ query.append(f"QUERY_WAREHOUSE = 'streamlit'")
113
+ else:
108
114
  query.append(f"QUERY_WAREHOUSE = {streamlit.query_warehouse}")
115
+
109
116
  if streamlit.title:
110
117
  query.append(f"TITLE = '{streamlit.title}'")
111
118
 
@@ -1,7 +1,17 @@
1
+ import functools
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from click import ClickException
6
+ from snowflake.cli._plugins.connection.util import make_snowsight_url
7
+ from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
1
8
  from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
2
9
  StreamlitEntityModel,
3
10
  )
4
- from snowflake.cli.api.entities.common import EntityBase
11
+ from snowflake.cli._plugins.workspace.context import ActionContext
12
+ from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
13
+ from snowflake.cli.api.secure_path import SecurePath
14
+ from snowflake.connector.cursor import SnowflakeCursor
5
15
 
6
16
 
7
17
  class StreamlitEntity(EntityBase[StreamlitEntityModel]):
@@ -9,4 +19,145 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
9
19
  A Streamlit app.
10
20
  """
11
21
 
12
- pass
22
+ def __init__(self, *args, **kwargs):
23
+ if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
24
+ raise NotImplementedError("Streamlit entity is not implemented yet")
25
+ super().__init__(*args, **kwargs)
26
+
27
+ @property
28
+ def root(self):
29
+ return self._workspace_ctx.project_root
30
+
31
+ @property
32
+ def artifacts(self):
33
+ return self._entity_model.artifacts
34
+
35
+ @functools.cached_property
36
+ def _sql_executor(self):
37
+ return get_sql_executor()
38
+
39
+ @functools.cached_property
40
+ def _conn(self):
41
+ return self._sql_executor._conn # noqa
42
+
43
+ @property
44
+ def model(self):
45
+ return self._entity_model # noqa
46
+
47
+ def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
48
+ return self.bundle()
49
+
50
+ def action_deploy(self, action_ctx: ActionContext, *args, **kwargs):
51
+ # After adding bundle map- we should use it's mapping here
52
+ # To copy artifacts to destination on stage.
53
+
54
+ return self._sql_executor.execute_query(self.get_deploy_sql())
55
+
56
+ def action_drop(self, action_ctx: ActionContext, *args, **kwargs):
57
+ return self._sql_executor.execute_query(self.get_drop_sql())
58
+
59
+ def action_execute(
60
+ self, action_ctx: ActionContext, *args, **kwargs
61
+ ) -> SnowflakeCursor:
62
+ return self._sql_executor.execute_query(self.get_execute_sql())
63
+
64
+ def action_get_url(
65
+ self, action_ctx: ActionContext, *args, **kwargs
66
+ ): # maybe this should be a property
67
+ name = self._entity_model.fqn.using_connection(self._conn)
68
+ return make_snowsight_url(
69
+ self._conn, f"/#/streamlit-apps/{name.url_identifier}"
70
+ )
71
+
72
+ def bundle(self, output_dir: Optional[Path] = None):
73
+
74
+ if not output_dir:
75
+ output_dir = self.root / "output" / self._entity_model.stage
76
+
77
+ artifacts = self._entity_model.artifacts
78
+
79
+ output_dir.mkdir(parents=True, exist_ok=True) # type: ignore
80
+
81
+ output_files = []
82
+
83
+ # This is far from , but will be replaced by bundlemap mappings.
84
+ for file in artifacts:
85
+ output_file = output_dir / file.name
86
+
87
+ if file.is_file():
88
+ SecurePath(file).copy(output_file)
89
+ elif file.is_dir():
90
+ output_file.mkdir(parents=True, exist_ok=True)
91
+ SecurePath(file).copy(output_file, dirs_exist_ok=True)
92
+
93
+ output_files.append(output_file)
94
+
95
+ return output_files
96
+
97
+ def action_share(
98
+ self, action_ctx: ActionContext, to_role: str, *args, **kwargs
99
+ ) -> SnowflakeCursor:
100
+ return self._sql_executor.execute_query(self.get_share_sql(to_role))
101
+
102
+ def get_deploy_sql(
103
+ self,
104
+ if_not_exists: bool = False,
105
+ replace: bool = False,
106
+ from_stage_name: Optional[str] = None,
107
+ artifacts_dir: Optional[Path] = None,
108
+ schema: Optional[str] = None,
109
+ *args,
110
+ **kwargs,
111
+ ):
112
+ if replace and if_not_exists:
113
+ raise ClickException("Cannot specify both replace and if_not_exists")
114
+
115
+ if replace:
116
+ query = "CREATE OR REPLACE "
117
+ elif if_not_exists:
118
+ query = "CREATE IF NOT EXISTS "
119
+ else:
120
+ query = "CREATE "
121
+
122
+ schema_to_use = schema or self._entity_model.fqn.schema
123
+ query += f"STREAMLIT {self._entity_model.fqn.set_schema(schema_to_use).sql_identifier}"
124
+
125
+ if from_stage_name:
126
+ query += f"\nROOT_LOCATION = '{from_stage_name}'"
127
+ elif artifacts_dir:
128
+ query += f"\nFROM '{artifacts_dir}'"
129
+
130
+ query += f"\nMAIN_FILE = '{self._entity_model.main_file}'"
131
+
132
+ if self.model.imports:
133
+ query += "\n" + self.model.get_imports_sql()
134
+
135
+ if self.model.query_warehouse:
136
+ query += f"\nQUERY_WAREHOUSE = '{self.model.query_warehouse}'"
137
+
138
+ if self.model.title:
139
+ query += f"\nTITLE = '{self.model.title}'"
140
+
141
+ if self.model.comment:
142
+ query += f"\nCOMMENT = '{self.model.comment}'"
143
+
144
+ if self.model.external_access_integrations:
145
+ query += "\n" + self.model.get_external_access_integrations_sql()
146
+
147
+ if self.model.secrets:
148
+ query += "\n" + self.model.get_secrets_sql()
149
+
150
+ return query + ";"
151
+
152
+ def get_share_sql(self, to_role: str) -> str:
153
+ return f"GRANT USAGE ON STREAMLIT {self.model.fqn.sql_identifier} TO ROLE {to_role};"
154
+
155
+ def get_execute_sql(self):
156
+ return f"EXECUTE STREAMLIT {self._entity_model.fqn}();"
157
+
158
+ def get_usage_grant_sql(self, app_role: str, schema: Optional[str] = None) -> str:
159
+ entity_id = self.entity_id
160
+ streamlit_name = f"{schema}.{entity_id}" if schema else entity_id
161
+ return (
162
+ f"GRANT USAGE ON STREAMLIT {streamlit_name} TO APPLICATION ROLE {app_role};"
163
+ )
@@ -34,7 +34,7 @@ from snowflake.cli.api.commands.decorators import with_project_definition
34
34
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
35
35
  from snowflake.cli.api.entities.common import EntityActions
36
36
  from snowflake.cli.api.exceptions import IncompatibleParametersError
37
- from snowflake.cli.api.output.types import MessageResult, QueryResult
37
+ from snowflake.cli.api.output.types import CollectionResult, MessageResult
38
38
 
39
39
  ws = SnowTyperFactory(
40
40
  name="ws",
@@ -243,7 +243,7 @@ def version_list(
243
243
  entity_id,
244
244
  EntityActions.VERSION_LIST,
245
245
  )
246
- return QueryResult(cursor)
246
+ return CollectionResult(cursor)
247
247
 
248
248
 
249
249
  @version.command(name="create", requires_connection=True, hidden=True)
@@ -293,6 +293,7 @@ def version_create(
293
293
  skip_git_check=skip_git_check,
294
294
  interactive=interactive,
295
295
  force=force,
296
+ from_stage=False,
296
297
  )
297
298
 
298
299
 
@@ -1,3 +1,4 @@
1
+ from functools import cached_property
1
2
  from pathlib import Path
2
3
  from typing import Dict
3
4
 
@@ -58,10 +59,7 @@ class WorkspaceManager:
58
59
  """
59
60
  entity = self.get_entity(entity_id)
60
61
  if entity.supports(action):
61
- action_ctx = ActionContext(
62
- get_entity=self.get_entity,
63
- )
64
- return entity.perform(action, action_ctx, *args, **kwargs)
62
+ return entity.perform(action, self.action_ctx, *args, **kwargs)
65
63
  else:
66
64
  raise ValueError(f'This entity type does not support "{action.value}"')
67
65
 
@@ -69,6 +67,12 @@ class WorkspaceManager:
69
67
  def project_root(self) -> Path:
70
68
  return self._project_root
71
69
 
70
+ @cached_property
71
+ def action_ctx(self) -> ActionContext:
72
+ return ActionContext(
73
+ get_entity=self.get_entity,
74
+ )
75
+
72
76
 
73
77
  def _get_default_role() -> str:
74
78
  role = default_role()