snowflake-cli 3.2.2__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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
- snowflake/cli/_plugins/nativeapp/commands.py +92 -2
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
- snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
- snowflake/cli/_plugins/notebook/manager.py +4 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +134 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/manager.py +12 -4
- snowflake/cli/_plugins/streamlit/manager.py +8 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
- snowflake/cli/_plugins/workspace/commands.py +3 -2
- snowflake/cli/_plugins/workspace/manager.py +8 -4
- snowflake/cli/api/cli_global_context.py +22 -1
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +9 -1
- snowflake/cli/api/entities/common.py +85 -0
- snowflake/cli/api/entities/utils.py +9 -8
- snowflake/cli/api/errno.py +60 -3
- snowflake/cli/api/feature_flags.py +20 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +1 -2
- snowflake/cli/api/project/schemas/project_definition.py +27 -6
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +12 -12
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +55 -50
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.2.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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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()
|