snowflake-cli 3.6.0__py3-none-any.whl → 3.7.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.
Files changed (46) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  3. snowflake/cli/_app/loggers.py +2 -2
  4. snowflake/cli/_app/snow_connector.py +2 -2
  5. snowflake/cli/_plugins/connection/commands.py +2 -1
  6. snowflake/cli/_plugins/helpers/commands.py +25 -1
  7. snowflake/cli/_plugins/helpers/snowsl_vars_reader.py +133 -0
  8. snowflake/cli/_plugins/init/commands.py +9 -6
  9. snowflake/cli/_plugins/logs/__init__.py +0 -0
  10. snowflake/cli/_plugins/logs/commands.py +105 -0
  11. snowflake/cli/_plugins/logs/manager.py +107 -0
  12. snowflake/cli/_plugins/logs/plugin_spec.py +16 -0
  13. snowflake/cli/_plugins/logs/utils.py +60 -0
  14. snowflake/cli/_plugins/notebook/commands.py +3 -0
  15. snowflake/cli/_plugins/notebook/notebook_entity.py +16 -27
  16. snowflake/cli/_plugins/project/commands.py +73 -48
  17. snowflake/cli/_plugins/project/manager.py +57 -23
  18. snowflake/cli/_plugins/project/project_entity_model.py +22 -3
  19. snowflake/cli/_plugins/snowpark/commands.py +15 -2
  20. snowflake/cli/_plugins/spcs/image_registry/manager.py +15 -6
  21. snowflake/cli/_plugins/sql/manager.py +4 -4
  22. snowflake/cli/_plugins/stage/manager.py +17 -10
  23. snowflake/cli/_plugins/streamlit/commands.py +3 -0
  24. snowflake/cli/_plugins/streamlit/manager.py +19 -15
  25. snowflake/cli/api/artifacts/upload.py +30 -34
  26. snowflake/cli/api/artifacts/utils.py +8 -6
  27. snowflake/cli/api/cli_global_context.py +7 -2
  28. snowflake/cli/api/commands/decorators.py +11 -2
  29. snowflake/cli/api/commands/flags.py +23 -2
  30. snowflake/cli/api/commands/snow_typer.py +20 -2
  31. snowflake/cli/api/config.py +5 -3
  32. snowflake/cli/api/entities/utils.py +29 -16
  33. snowflake/cli/api/exceptions.py +69 -28
  34. snowflake/cli/api/identifiers.py +2 -0
  35. snowflake/cli/api/plugins/plugin_config.py +2 -2
  36. snowflake/cli/api/project/schemas/template.py +3 -3
  37. snowflake/cli/api/rendering/project_templates.py +3 -3
  38. snowflake/cli/api/rendering/sql_templates.py +2 -2
  39. snowflake/cli/api/sql_execution.py +1 -1
  40. snowflake/cli/api/utils/definition_rendering.py +14 -8
  41. snowflake/cli/api/utils/templating_functions.py +4 -4
  42. {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.1.dist-info}/METADATA +9 -8
  43. {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.1.dist-info}/RECORD +46 -40
  44. {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.1.dist-info}/WHEEL +0 -0
  45. {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.1.dist-info}/entry_points.txt +0 -0
  46. {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from enum import Enum, unique
18
18
 
19
- VERSION = "3.6.0"
19
+ VERSION = "3.7.1"
20
20
 
21
21
 
22
22
  @unique
@@ -18,6 +18,7 @@ from snowflake.cli._plugins.cortex import plugin_spec as cortex_plugin_spec
18
18
  from snowflake.cli._plugins.git import plugin_spec as git_plugin_spec
19
19
  from snowflake.cli._plugins.helpers import plugin_spec as migrate_plugin_spec
20
20
  from snowflake.cli._plugins.init import plugin_spec as init_plugin_spec
21
+ from snowflake.cli._plugins.logs import plugin_spec as logs_plugin_spec
21
22
  from snowflake.cli._plugins.nativeapp import plugin_spec as nativeapp_plugin_spec
22
23
  from snowflake.cli._plugins.notebook import plugin_spec as notebook_plugin_spec
23
24
  from snowflake.cli._plugins.object import plugin_spec as object_plugin_spec
@@ -51,6 +52,7 @@ def get_builtin_plugin_name_to_plugin_spec():
51
52
  "init": init_plugin_spec,
52
53
  "workspace": workspace_plugin_spec,
53
54
  "plugin": plugin_plugin_spec,
55
+ "logs": logs_plugin_spec,
54
56
  }
55
57
 
56
58
  return plugin_specs
@@ -20,7 +20,7 @@ from dataclasses import asdict, dataclass, field
20
20
  from typing import Any, Dict, List
21
21
 
22
22
  import typer
23
- from snowflake.cli.api.exceptions import InvalidLogsConfiguration
23
+ from snowflake.cli.api.exceptions import InvalidLogsConfigurationError
24
24
  from snowflake.cli.api.secure_path import SecurePath
25
25
  from snowflake.connector.errors import ConfigSourceError
26
26
 
@@ -129,7 +129,7 @@ class FileLogsConfig:
129
129
  logging.CRITICAL,
130
130
  ]
131
131
  if self.level not in possible_log_levels:
132
- raise InvalidLogsConfiguration(
132
+ raise InvalidLogsConfigurationError(
133
133
  f"Invalid 'level' value set in [logs] section: {config['level']}. "
134
134
  f"'level' should be one of: {' / '.join(logging.getLevelName(lvl) for lvl in possible_log_levels)}"
135
135
  )
@@ -33,7 +33,7 @@ from snowflake.cli.api.config import (
33
33
  )
34
34
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
35
35
  from snowflake.cli.api.exceptions import (
36
- InvalidConnectionConfiguration,
36
+ InvalidConnectionConfigurationError,
37
37
  SnowflakeConnectionError,
38
38
  )
39
39
  from snowflake.cli.api.feature_flags import FeatureFlag
@@ -169,7 +169,7 @@ def connect_to_snowflake(
169
169
  except ForbiddenError as err:
170
170
  raise SnowflakeConnectionError(err)
171
171
  except DatabaseError as err:
172
- raise InvalidConnectionConfiguration(err.msg)
172
+ raise InvalidConnectionConfigurationError(err.msg)
173
173
 
174
174
 
175
175
  def _avoid_closing_the_connection_if_it_was_shared(
@@ -66,6 +66,7 @@ from snowflake.cli.api.config import (
66
66
  )
67
67
  from snowflake.cli.api.console import cli_console
68
68
  from snowflake.cli.api.constants import ObjectType
69
+ from snowflake.cli.api.feature_flags import FeatureFlag
69
70
  from snowflake.cli.api.output.types import (
70
71
  CollectionResult,
71
72
  CommandResult,
@@ -289,7 +290,7 @@ def add(
289
290
  if connection_exists(connection_name):
290
291
  raise UsageError(f"Connection {connection_name} already exists")
291
292
 
292
- if not no_interactive:
293
+ if FeatureFlag.ENABLE_AUTH_KEYPAIR.is_enabled() and not no_interactive:
293
294
  connection_options, keypair_error = _extend_add_with_key_pair(
294
295
  connection_name, connection_options
295
296
  )
@@ -15,12 +15,14 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import logging
18
+ import os
18
19
  from pathlib import Path
19
20
  from typing import Any, List, Optional
20
21
 
21
22
  import typer
22
23
  import yaml
23
24
  from click import ClickException
25
+ from snowflake.cli._plugins.helpers.snowsl_vars_reader import check_env_vars
24
26
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
25
27
  from snowflake.cli.api.config import (
26
28
  ConnectionConfig,
@@ -29,7 +31,12 @@ from snowflake.cli.api.config import (
29
31
  set_config_value,
30
32
  )
31
33
  from snowflake.cli.api.console import cli_console
32
- from snowflake.cli.api.output.types import CommandResult, MessageResult
34
+ from snowflake.cli.api.output.types import (
35
+ CollectionResult,
36
+ CommandResult,
37
+ MessageResult,
38
+ MultipleResults,
39
+ )
33
40
  from snowflake.cli.api.project.definition_conversion import (
34
41
  convert_project_definition_to_v2,
35
42
  )
@@ -293,3 +300,20 @@ def _validate_and_save_connections_imported_from_snowsql(
293
300
  path=["default_connection_name"],
294
301
  value=default_cli_connection_name,
295
302
  )
303
+
304
+
305
+ @app.command(name="check-snowsql-env-vars", requires_connection=False)
306
+ def check_snowsql_env_vars(**options):
307
+ """Check if there are any SnowSQL environment variables set."""
308
+
309
+ env_vars = os.environ.copy()
310
+ discovered, unused, summary = check_env_vars(env_vars)
311
+
312
+ results = []
313
+ if discovered:
314
+ results.append(CollectionResult(discovered))
315
+ if unused:
316
+ results.append(CollectionResult(unused))
317
+
318
+ results.append(MessageResult(summary))
319
+ return MultipleResults(results)
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ EnvVars = dict[str, str]
4
+
5
+ DicoveredVars = list[dict[str, str]]
6
+ UnusedVars = list[dict[str, str]]
7
+ Summary = str
8
+
9
+ CheckResult = tuple[DicoveredVars, UnusedVars, Summary]
10
+
11
+ KNOWN_SNOWSQL_ENV_VARS = {
12
+ "SNOWSQL_ACCOUNT": {
13
+ "Found": "SNOWSQL_ACCOUNT",
14
+ "Suggested": "SNOWFLAKE_ACCOUNT",
15
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
16
+ },
17
+ "SNOWSQL_PWD": {
18
+ "Found": "SNOWSQL_PASSWORD",
19
+ "Suggested": "SNOWFLAKE_PASSWORD",
20
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
21
+ },
22
+ "SNOWSQL_USER": {
23
+ "Found": "SNOWSQL_USER",
24
+ "Suggested": "SNOWFLAKE_USER",
25
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
26
+ },
27
+ "SNOWSQL_REGION": {
28
+ "Found": "SNOWSQL_REGION",
29
+ "Suggested": "SNOWFLAKE_REGION",
30
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
31
+ },
32
+ "SNOWSQL_ROLE": {
33
+ "Found": "SNOWSQL_ROLE",
34
+ "Suggested": "SNOWFLAKE_ROLE",
35
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
36
+ },
37
+ "SNOWSQL_WAREHOUSE": {
38
+ "Found": "SNOWSQL_WAREHOUSE",
39
+ "Suggested": "SNOWFLAKE_WAREHOUSE",
40
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
41
+ },
42
+ "SNOWSQL_DATABASE": {
43
+ "Found": "SNOWSQL_DATABASE",
44
+ "Suggested": "SNOWFLAKE_DATABASE",
45
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
46
+ },
47
+ "SNOWSQL_SCHEMA": {
48
+ "found": "SNOWSQL_SCHEMA",
49
+ "Suggested": "SNOWFLAKE_SCHEMA",
50
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
51
+ },
52
+ "SNOWSQL_HOST": {
53
+ "Found": "SNOWSQL_HOST",
54
+ "Suggested": "SNOWFLAKE_HOST",
55
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
56
+ },
57
+ "SNOWSQL_PORT": {
58
+ "Found": "SNOWSQL_PORT",
59
+ "Suggested": "SNOWFLAKE_PORT",
60
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
61
+ },
62
+ "SNOWSQL_PROTOCOL": {
63
+ "Found": "SNOWSQL_PROTOCOL",
64
+ "Suggested": "SNOWFLAKE_PROTOCOL",
65
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections#use-environment-variables-for-snowflake-credentials",
66
+ },
67
+ "SNOWSQL_PROXY_HOST": {
68
+ "Found": "SNOWSQL_PROXY_HOST",
69
+ "Suggested": "PROXY_HOST",
70
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#use-a-proxy-server",
71
+ },
72
+ "SNOWSQL_PROXY_PORT": {
73
+ "Found": "SNOWSQL_PROXY_HOST",
74
+ "Suggested": "PROXY_HOST",
75
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#use-a-proxy-server",
76
+ },
77
+ "SNOWSQL_PROXY_USER": {
78
+ "Found": "SNOWSQL_PROXY_PORT",
79
+ "Suggested": "PROXY_PORT",
80
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#use-a-proxy-server",
81
+ },
82
+ "SNOWSQL_PROXY_PWD": {
83
+ "Found": "SNOWSQL_PROXY_PWD",
84
+ "Suggested": "PROXY_PWD",
85
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#use-a-proxy-server",
86
+ },
87
+ "SNOWSQL_PRIVATE_KEY_PASSPHRASE": {
88
+ "Found": "SNOWSQL_PRIVATE_KEY_PASSPHRASE",
89
+ "Suggested": "PRIVATE_KEY_PASSPHRASE",
90
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-connections",
91
+ },
92
+ "EXIT_ON_ERROR": {
93
+ "Found": "EXIT_ON_ERROR",
94
+ "Suggested": "SNOWFLAKE_ENHANCED_EXIT_CODES",
95
+ "Additional info": "https://docs.snowflake.com/en/developer-guide/snowflake-cli/sql/execute-sql",
96
+ },
97
+ }
98
+
99
+
100
+ def check_env_vars(variables: EnvVars) -> CheckResult:
101
+ """Checks passed dict objects for possible SnowSQL variables.
102
+
103
+ Returns tuple of
104
+ - sequence of variables that can be adjusted
105
+ - sequence of variables that have no corresponding variables
106
+ - a summary messages
107
+ """
108
+ discovered: DicoveredVars = []
109
+ unused: UnusedVars = []
110
+
111
+ prefix_matched = (e for e in variables if e.lower().startswith("snowsql"))
112
+
113
+ for var in prefix_matched:
114
+ if suggestion := KNOWN_SNOWSQL_ENV_VARS.get(var, None):
115
+ discovered.append(suggestion)
116
+ else:
117
+ unused.append(
118
+ {
119
+ "Found": var,
120
+ "Suggested": "n/a",
121
+ "Additional info": "Unused variable",
122
+ }
123
+ )
124
+
125
+ discovered_cnt = len(discovered)
126
+ unused_cnt = len(unused)
127
+
128
+ summary: Summary = (
129
+ f"Found {discovered_cnt + unused_cnt} SnowSQL environment variables,"
130
+ f" {discovered_cnt} with replacements, {unused_cnt} unused."
131
+ )
132
+
133
+ return discovered, unused, summary
@@ -28,7 +28,7 @@ from snowflake.cli.api.commands.flags import (
28
28
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
29
29
  from snowflake.cli.api.commands.utils import parse_key_value_variables
30
30
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
31
- from snowflake.cli.api.exceptions import InvalidTemplate
31
+ from snowflake.cli.api.exceptions import InvalidTemplateError
32
32
  from snowflake.cli.api.output.types import (
33
33
  CommandResult,
34
34
  MessageResult,
@@ -138,7 +138,7 @@ def _read_template_metadata(template_root: SecurePath, args_error_msg: str) -> T
138
138
  template_metadata_path = template_root / TEMPLATE_METADATA_FILE_NAME
139
139
  log.debug("Reading template metadata from %s", template_metadata_path.path)
140
140
  if not template_metadata_path.exists():
141
- raise InvalidTemplate(
141
+ raise InvalidTemplateError(
142
142
  f"File {TEMPLATE_METADATA_FILE_NAME} not found. {args_error_msg}"
143
143
  )
144
144
  with template_metadata_path.open(read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as fd:
@@ -201,8 +201,9 @@ def init(
201
201
  variables_from_flags = {
202
202
  v.key: v.value for v in parse_key_value_variables(variables)
203
203
  }
204
- is_remote = any(
205
- template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
204
+ is_remote = template_source is not None and any(
205
+ template_source.startswith(prefix)
206
+ for prefix in ["git@", "http://", "https://"] # type: ignore
206
207
  )
207
208
  args_error_msg = f"Check whether {TemplateOption.param_decls[0]} and {SourceOption.param_decls[0]} arguments are correct."
208
209
 
@@ -210,11 +211,13 @@ def init(
210
211
  with SecurePath.temporary_directory() as tmpdir:
211
212
  if is_remote:
212
213
  template_root = _fetch_remote_template(
213
- url=template_source, path=template, destination=tmpdir # type: ignore
214
+ url=template_source, # type: ignore
215
+ path=template,
216
+ destination=tmpdir, # type: ignore
214
217
  )
215
218
  else:
216
219
  template_root = _fetch_local_template(
217
- template_source=SecurePath(template_source),
220
+ template_source=SecurePath(template_source), # type: ignore
218
221
  path=template,
219
222
  destination=tmpdir,
220
223
  )
File without changes
@@ -0,0 +1,105 @@
1
+ import itertools
2
+ from datetime import datetime
3
+ from typing import Generator, Iterable, Optional, cast
4
+
5
+ import typer
6
+ from click import ClickException
7
+ from snowflake.cli._plugins.logs.manager import LogsManager
8
+ from snowflake.cli._plugins.logs.utils import LOG_LEVELS, LogsQueryRow
9
+ from snowflake.cli._plugins.object.commands import NameArgument, ObjectArgument
10
+ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
11
+ from snowflake.cli.api.exceptions import CliArgumentError
12
+ from snowflake.cli.api.identifiers import FQN
13
+ from snowflake.cli.api.output.types import (
14
+ CommandResult,
15
+ MessageResult,
16
+ StreamResult,
17
+ )
18
+
19
+ app = SnowTyperFactory()
20
+
21
+
22
+ @app.command(name="logs", requires_connection=True)
23
+ def get_logs(
24
+ object_type: str = ObjectArgument,
25
+ object_name: FQN = NameArgument,
26
+ from_: Optional[str] = typer.Option(
27
+ None,
28
+ "--from",
29
+ help="The start time of the logs to retrieve. Accepts all ISO8061 formats",
30
+ ),
31
+ to: Optional[str] = typer.Option(
32
+ None,
33
+ "--to",
34
+ help="The end time of the logs to retrieve. Accepts all ISO8061 formats",
35
+ ),
36
+ refresh_time: int = typer.Option(
37
+ None,
38
+ "--refresh",
39
+ help="If set, the logs will be streamed with the given refresh time in seconds",
40
+ ),
41
+ event_table: Optional[str] = typer.Option(
42
+ None,
43
+ "--table",
44
+ help="The table to query for logs. If not provided, the default table will be used",
45
+ ),
46
+ log_level: Optional[str] = typer.Option(
47
+ "INFO",
48
+ "--log-level",
49
+ help="The log level to filter by. If not provided, INFO will be used",
50
+ ),
51
+ **options,
52
+ ):
53
+ """
54
+ Retrieves logs for a given object.
55
+ """
56
+
57
+ if log_level and not log_level.upper() in LOG_LEVELS:
58
+ raise CliArgumentError(
59
+ f"Invalid log level. Please choose from {', '.join(LOG_LEVELS)}"
60
+ )
61
+
62
+ if refresh_time and to:
63
+ raise ClickException(
64
+ "You cannot set both --refresh and --to parameters. Please check the values"
65
+ )
66
+
67
+ from_time = get_datetime_from_string(from_, "--from") if from_ else None
68
+ to_time = get_datetime_from_string(to, "--to") if to else None
69
+
70
+ if refresh_time:
71
+ logs_stream: Iterable[LogsQueryRow] = LogsManager().stream_logs(
72
+ object_type=object_type,
73
+ object_name=object_name,
74
+ from_time=from_time,
75
+ refresh_time=refresh_time,
76
+ event_table=event_table,
77
+ log_level=log_level,
78
+ )
79
+ logs = itertools.chain(
80
+ (MessageResult(log.log_message) for logs in logs_stream for log in logs)
81
+ )
82
+ else:
83
+ logs_iterable: Iterable[LogsQueryRow] = LogsManager().get_logs(
84
+ object_type=object_type,
85
+ object_name=object_name,
86
+ from_time=from_time,
87
+ to_time=to_time,
88
+ event_table=event_table,
89
+ log_level=log_level,
90
+ )
91
+ logs = (MessageResult(log.log_message) for log in logs_iterable) # type: ignore
92
+
93
+ return StreamResult(cast(Generator[CommandResult, None, None], logs))
94
+
95
+
96
+ def get_datetime_from_string(
97
+ date_str: str,
98
+ name: Optional[str] = None,
99
+ ) -> datetime:
100
+ try:
101
+ return datetime.fromisoformat(date_str)
102
+ except ValueError:
103
+ raise ClickException(
104
+ f"Incorrect format for '{name}'. Please use one of approved ISO formats."
105
+ )
@@ -0,0 +1,107 @@
1
+ import time
2
+ from datetime import datetime
3
+ from textwrap import dedent
4
+ from typing import Iterable, List, Optional
5
+
6
+ from snowflake.cli._plugins.logs.utils import (
7
+ LogsQueryRow,
8
+ get_timestamp_query,
9
+ parse_log_levels_for_query,
10
+ sanitize_logs,
11
+ )
12
+ from snowflake.cli._plugins.object.commands import NameArgument, ObjectArgument
13
+ from snowflake.cli.api.identifiers import FQN
14
+ from snowflake.cli.api.sql_execution import SqlExecutionMixin
15
+ from snowflake.connector.cursor import SnowflakeCursor
16
+
17
+
18
+ class LogsManager(SqlExecutionMixin):
19
+ def stream_logs(
20
+ self,
21
+ refresh_time: int,
22
+ object_type: str = ObjectArgument,
23
+ object_name: FQN = NameArgument,
24
+ from_time: Optional[datetime] = None,
25
+ event_table: Optional[str] = None,
26
+ log_level: Optional[str] = "INFO",
27
+ ) -> Iterable[List[LogsQueryRow]]:
28
+ try:
29
+ previous_end = from_time
30
+
31
+ while True:
32
+ raw_logs = self.get_raw_logs(
33
+ object_type=object_type,
34
+ object_name=object_name,
35
+ from_time=previous_end,
36
+ to_time=None,
37
+ event_table=event_table,
38
+ log_level=log_level,
39
+ ).fetchall()
40
+
41
+ if raw_logs:
42
+ result = self.sanitize_logs(raw_logs)
43
+ yield result
44
+ if result:
45
+ previous_end = result[-1].timestamp
46
+ time.sleep(refresh_time)
47
+
48
+ except KeyboardInterrupt:
49
+ return
50
+
51
+ def get_logs(
52
+ self,
53
+ object_type: str = ObjectArgument,
54
+ object_name: FQN = NameArgument,
55
+ from_time: Optional[datetime] = None,
56
+ to_time: Optional[datetime] = None,
57
+ event_table: Optional[str] = None,
58
+ log_level: Optional[str] = "INFO",
59
+ ) -> Iterable[LogsQueryRow]:
60
+ """
61
+ Basic function to get a single batch of logs from the server
62
+ """
63
+
64
+ logs = self.get_raw_logs(
65
+ object_type=object_type,
66
+ object_name=object_name,
67
+ from_time=from_time,
68
+ to_time=to_time,
69
+ event_table=event_table,
70
+ log_level=log_level,
71
+ )
72
+
73
+ return sanitize_logs(logs)
74
+
75
+ def get_raw_logs(
76
+ self,
77
+ object_type: str = ObjectArgument,
78
+ object_name: FQN = NameArgument,
79
+ from_time: Optional[datetime] = None,
80
+ to_time: Optional[datetime] = None,
81
+ event_table: Optional[str] = None,
82
+ log_level: Optional[str] = "INFO",
83
+ ) -> SnowflakeCursor:
84
+
85
+ table = event_table if event_table else "SNOWFLAKE.TELEMETRY.EVENTS"
86
+
87
+ query = dedent(
88
+ f"""
89
+ SELECT
90
+ timestamp,
91
+ resource_attributes:"snow.database.name"::string as database_name,
92
+ resource_attributes:"snow.schema.name"::string as schema_name,
93
+ resource_attributes:"snow.{object_type}.name"::string as object_name,
94
+ record:severity_text::string as log_level,
95
+ value::string as log_message
96
+ FROM {table}
97
+ WHERE record_type = 'LOG'
98
+ AND (record:severity_text IN ({parse_log_levels_for_query((log_level))}) or record:severity_text is NULL )
99
+ AND object_name = '{object_name}'
100
+ {get_timestamp_query(from_time, to_time)}
101
+ ORDER BY timestamp;
102
+ """
103
+ ).strip()
104
+
105
+ result = self.execute_query(query)
106
+
107
+ return result
@@ -0,0 +1,16 @@
1
+ from snowflake.cli._plugins.logs import commands
2
+ from snowflake.cli.api.plugins.command import (
3
+ SNOWCLI_ROOT_COMMAND_PATH,
4
+ CommandSpec,
5
+ CommandType,
6
+ plugin_hook_impl,
7
+ )
8
+
9
+
10
+ @plugin_hook_impl
11
+ def command_spec():
12
+ return CommandSpec(
13
+ parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
14
+ command_type=CommandType.SINGLE_COMMAND,
15
+ typer_instance=commands.app.create_instance(),
16
+ )
@@ -0,0 +1,60 @@
1
+ from datetime import datetime
2
+ from typing import List, NamedTuple, Optional, Tuple
3
+
4
+ from snowflake.cli.api.exceptions import CliArgumentError, CliSqlError
5
+ from snowflake.connector.cursor import SnowflakeCursor
6
+
7
+ LOG_LEVELS = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
8
+
9
+ LogsQueryRow = NamedTuple(
10
+ "LogsQueryRow",
11
+ [
12
+ ("timestamp", datetime),
13
+ ("database_name", str),
14
+ ("schema_name", str),
15
+ ("object_name", str),
16
+ ("log_level", str),
17
+ ("log_message", str),
18
+ ],
19
+ )
20
+
21
+
22
+ def sanitize_logs(logs: SnowflakeCursor | List[Tuple]) -> List[LogsQueryRow]:
23
+ try:
24
+ return [LogsQueryRow(*log) for log in logs]
25
+ except TypeError:
26
+ raise CliSqlError(
27
+ "Logs table has incorrect format. Please check the logs_table in your database"
28
+ )
29
+
30
+
31
+ def get_timestamp_query(from_time: Optional[datetime], to_time: Optional[datetime]):
32
+ if from_time and to_time and from_time > to_time:
33
+ raise CliArgumentError(
34
+ "From_time cannot be later than to_time. Please check the values"
35
+ )
36
+ query = []
37
+
38
+ if from_time is not None:
39
+ query.append(f"AND timestamp >= TO_TIMESTAMP_LTZ('{from_time.isoformat()}')\n")
40
+
41
+ if to_time is not None:
42
+ query.append(f"AND timestamp <= TO_TIMESTAMP_LTZ('{to_time.isoformat()}')\n")
43
+
44
+ return "".join(query)
45
+
46
+
47
+ def get_log_levels(log_level: str):
48
+ if log_level.upper() not in LOG_LEVELS and log_level != "":
49
+ raise CliArgumentError(
50
+ f"Invalid log level. Please choose from {', '.join(LOG_LEVELS)}"
51
+ )
52
+
53
+ if log_level == "":
54
+ log_level = "INFO"
55
+
56
+ return LOG_LEVELS[LOG_LEVELS.index(log_level.upper()) :]
57
+
58
+
59
+ def parse_log_levels_for_query(log_level: str):
60
+ return ", ".join(f"'{level}'" for level in get_log_levels(log_level))
@@ -23,6 +23,7 @@ from snowflake.cli._plugins.workspace.manager import WorkspaceManager
23
23
  from snowflake.cli.api.cli_global_context import get_cli_context
24
24
  from snowflake.cli.api.commands.decorators import with_project_definition
25
25
  from snowflake.cli.api.commands.flags import (
26
+ PruneOption,
26
27
  ReplaceOption,
27
28
  entity_argument,
28
29
  identifier_argument,
@@ -108,6 +109,7 @@ def deploy(
108
109
  help="Replace notebook object if it already exists. It only uploads new and overwrites existing files, "
109
110
  "but does not remove any files already on the stage.",
110
111
  ),
112
+ prune: bool = PruneOption(),
111
113
  **options,
112
114
  ) -> CommandResult:
113
115
  """Uploads a notebook and required files to a stage and creates a Snowflake notebook."""
@@ -132,6 +134,7 @@ def deploy(
132
134
  notebook.entity_id,
133
135
  EntityActions.DEPLOY,
134
136
  replace=replace,
137
+ prune=prune,
135
138
  )
136
139
  return MessageResult(
137
140
  f"Notebook successfully deployed and available under {notebook_url}"