snowflake-cli 3.6.0__py3-none-any.whl → 3.7.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/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/loggers.py +2 -2
- snowflake/cli/_app/snow_connector.py +2 -2
- snowflake/cli/_plugins/connection/commands.py +2 -1
- snowflake/cli/_plugins/helpers/commands.py +25 -1
- snowflake/cli/_plugins/helpers/snowsl_vars_reader.py +133 -0
- snowflake/cli/_plugins/init/commands.py +9 -6
- snowflake/cli/_plugins/logs/__init__.py +0 -0
- snowflake/cli/_plugins/logs/commands.py +105 -0
- snowflake/cli/_plugins/logs/manager.py +107 -0
- snowflake/cli/_plugins/logs/plugin_spec.py +16 -0
- snowflake/cli/_plugins/logs/utils.py +60 -0
- snowflake/cli/_plugins/notebook/commands.py +3 -0
- snowflake/cli/_plugins/notebook/notebook_entity.py +16 -27
- snowflake/cli/_plugins/project/commands.py +73 -48
- snowflake/cli/_plugins/project/manager.py +57 -23
- snowflake/cli/_plugins/project/project_entity_model.py +22 -3
- snowflake/cli/_plugins/snowpark/commands.py +15 -2
- snowflake/cli/_plugins/sql/manager.py +4 -4
- snowflake/cli/_plugins/stage/manager.py +17 -10
- snowflake/cli/_plugins/streamlit/commands.py +3 -0
- snowflake/cli/_plugins/streamlit/manager.py +19 -15
- snowflake/cli/api/artifacts/upload.py +30 -34
- snowflake/cli/api/artifacts/utils.py +8 -6
- snowflake/cli/api/cli_global_context.py +7 -2
- snowflake/cli/api/commands/decorators.py +11 -2
- snowflake/cli/api/commands/flags.py +23 -2
- snowflake/cli/api/commands/snow_typer.py +20 -2
- snowflake/cli/api/config.py +5 -3
- snowflake/cli/api/entities/utils.py +29 -16
- snowflake/cli/api/exceptions.py +69 -28
- snowflake/cli/api/identifiers.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +2 -2
- snowflake/cli/api/project/schemas/template.py +3 -3
- snowflake/cli/api/rendering/project_templates.py +3 -3
- snowflake/cli/api/rendering/sql_templates.py +2 -2
- snowflake/cli/api/sql_execution.py +1 -1
- snowflake/cli/api/utils/definition_rendering.py +14 -8
- snowflake/cli/api/utils/templating_functions.py +4 -4
- {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.0.dist-info}/METADATA +8 -8
- {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.0.dist-info}/RECORD +45 -39
- {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.6.0.dist-info → snowflake_cli-3.7.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -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
|
snowflake/cli/_app/loggers.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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,
|
|
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}"
|