snowflake-cli 3.4.1__py3-none-any.whl → 3.6.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 +13 -1
- snowflake/cli/_app/cli_app.py +1 -10
- snowflake/cli/_app/commands_registration/builtin_plugins.py +7 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_app/snow_connector.py +5 -4
- snowflake/cli/_app/telemetry.py +3 -15
- snowflake/cli/_app/version_check.py +4 -4
- snowflake/cli/_plugins/auth/__init__.py +11 -0
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
- snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
- snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +79 -5
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
- snowflake/cli/_plugins/notebook/commands.py +3 -4
- snowflake/cli/_plugins/object/command_aliases.py +3 -1
- snowflake/cli/_plugins/object/manager.py +4 -2
- snowflake/cli/_plugins/plugin/commands.py +79 -0
- snowflake/cli/_plugins/plugin/manager.py +74 -0
- snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/__init__.py +0 -0
- snowflake/cli/_plugins/project/commands.py +173 -0
- snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -0
- snowflake/cli/_plugins/project/manager.py +76 -0
- snowflake/cli/_plugins/project/plugin_spec.py +30 -0
- snowflake/cli/_plugins/project/project_entity_model.py +40 -0
- snowflake/cli/_plugins/snowpark/commands.py +2 -1
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +70 -10
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
- snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +53 -0
- snowflake/cli/_plugins/spcs/services/manager.py +114 -0
- snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
- snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
- snowflake/cli/_plugins/sql/manager.py +42 -51
- snowflake/cli/_plugins/sql/source_reader.py +230 -0
- snowflake/cli/_plugins/stage/manager.py +10 -4
- snowflake/cli/_plugins/streamlit/commands.py +9 -24
- snowflake/cli/_plugins/streamlit/manager.py +5 -36
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/commands/flags.py +35 -10
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +2 -0
- snowflake/cli/api/config.py +15 -10
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/exceptions.py +15 -1
- snowflake/cli/api/feature_flags.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_helper.py +31 -0
- snowflake/cli/api/project/schemas/entities/entities.py +26 -0
- snowflake/cli/api/rest_api.py +2 -3
- snowflake/cli/{_app → api}/secret.py +4 -1
- snowflake/cli/api/secure_path.py +16 -4
- snowflake/cli/api/sql_execution.py +7 -3
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +12 -12
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +71 -50
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,23 +14,29 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import logging
|
|
17
18
|
import sys
|
|
18
|
-
from
|
|
19
|
-
from itertools import chain
|
|
19
|
+
from functools import partial
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Dict, Iterable, List, Tuple
|
|
22
22
|
|
|
23
23
|
from click import ClickException, UsageError
|
|
24
|
-
from jinja2 import UndefinedError
|
|
25
24
|
from snowflake.cli._plugins.sql.snowsql_templating import transpile_snowsql_templates
|
|
25
|
+
from snowflake.cli._plugins.sql.source_reader import (
|
|
26
|
+
compile_statements,
|
|
27
|
+
files_reader,
|
|
28
|
+
query_reader,
|
|
29
|
+
)
|
|
30
|
+
from snowflake.cli.api.console import cli_console
|
|
26
31
|
from snowflake.cli.api.rendering.sql_templates import snowflake_sql_jinja_render
|
|
27
|
-
from snowflake.cli.api.secure_path import
|
|
32
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
28
33
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin, VerboseCursor
|
|
29
34
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
30
|
-
from snowflake.connector.util_text import split_statements
|
|
31
35
|
|
|
32
36
|
IsSingleStatement = bool
|
|
33
37
|
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
34
40
|
|
|
35
41
|
class SqlManager(SqlExecutionMixin):
|
|
36
42
|
def execute(
|
|
@@ -41,57 +47,42 @@ class SqlManager(SqlExecutionMixin):
|
|
|
41
47
|
data: Dict | None = None,
|
|
42
48
|
retain_comments: bool = False,
|
|
43
49
|
) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
|
|
44
|
-
|
|
45
|
-
# Check if any two inputs were provided simultaneously
|
|
46
|
-
if len([i for i in inputs if i]) > 1:
|
|
47
|
-
raise UsageError(
|
|
48
|
-
"Multiple input sources specified. Please specify only one."
|
|
49
|
-
)
|
|
50
|
+
"""Reads, transforms and execute statements from input.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return self._execute_single_query(
|
|
55
|
-
query=query, data=data, retain_comments=retain_comments
|
|
56
|
-
)
|
|
52
|
+
Only one input can be consumed at a time.
|
|
53
|
+
When no compilation errors are detected, the sequence on queries
|
|
54
|
+
in executed and returned as tuple.
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
single_statement = False
|
|
62
|
-
for file in files:
|
|
63
|
-
query_from_file = SecurePath(file).read_text(
|
|
64
|
-
file_size_limit_mb=UNLIMITED
|
|
65
|
-
)
|
|
66
|
-
single_statement, result = self._execute_single_query(
|
|
67
|
-
query=query_from_file, data=data, retain_comments=retain_comments
|
|
68
|
-
)
|
|
69
|
-
results.append(result)
|
|
56
|
+
Throws an exception ff multiple inputs are provided.
|
|
57
|
+
"""
|
|
58
|
+
query = sys.stdin.read() if std_in else query
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
stmt_operators = (
|
|
61
|
+
transpile_snowsql_templates,
|
|
62
|
+
partial(snowflake_sql_jinja_render, data=data),
|
|
63
|
+
)
|
|
64
|
+
remove_comments = not retain_comments
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
if query:
|
|
67
|
+
stmt_reader = query_reader(query, stmt_operators, remove_comments)
|
|
68
|
+
elif files:
|
|
69
|
+
secured_files = [SecurePath(f) for f in files]
|
|
70
|
+
stmt_reader = files_reader(secured_files, stmt_operators, remove_comments)
|
|
71
|
+
else:
|
|
72
|
+
raise UsageError("Use either query, filename or input option.")
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
query = transpile_snowsql_templates(query)
|
|
83
|
-
query = snowflake_sql_jinja_render(content=query, data=data)
|
|
84
|
-
except UndefinedError as err:
|
|
85
|
-
raise ClickException(f"SQL template rendering error: {err}")
|
|
74
|
+
errors, stmt_count, compiled_statements = compile_statements(stmt_reader)
|
|
75
|
+
if not any((errors, stmt_count, compiled_statements)):
|
|
76
|
+
raise UsageError("Use either query, filename or input option.")
|
|
86
77
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
)
|
|
93
|
-
single_statement = len(statements) == 1
|
|
78
|
+
if errors:
|
|
79
|
+
for error in errors:
|
|
80
|
+
logger.info("Statement compilation error: %s", error)
|
|
81
|
+
cli_console.warning(error)
|
|
82
|
+
raise ClickException("SQL rendering error")
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
is_single_statement = not (stmt_count > 1)
|
|
85
|
+
return is_single_statement, self.execute_string(
|
|
86
|
+
"\n".join(compiled_statements),
|
|
87
|
+
cursor_class=VerboseCursor,
|
|
97
88
|
)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import io
|
|
3
|
+
import re
|
|
4
|
+
import urllib.error
|
|
5
|
+
from typing import Any, Callable, Generator, Literal, Sequence
|
|
6
|
+
from urllib.request import urlopen
|
|
7
|
+
|
|
8
|
+
from jinja2 import UndefinedError
|
|
9
|
+
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
10
|
+
from snowflake.connector.util_text import split_statements
|
|
11
|
+
|
|
12
|
+
SOURCE_PATTERN = re.compile(
|
|
13
|
+
r"^!(source|load)\s+[\"']?(.*?)[\"']?\s*(?:;|$)",
|
|
14
|
+
flags=re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
URL_PATTERN = re.compile(r"^(\w+?):\/(\/.*)", flags=re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
SplitedStatements = Generator[
|
|
20
|
+
tuple[str, bool | None] | tuple[str, Literal[False]],
|
|
21
|
+
Any,
|
|
22
|
+
None,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
SqlTransformFunc = Callable[[str], str]
|
|
26
|
+
OperatorFunctions = Sequence[SqlTransformFunc]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SourceType(enum.Enum):
|
|
30
|
+
FILE = "file"
|
|
31
|
+
QUERY = "query"
|
|
32
|
+
UNKNOWN = "unknown"
|
|
33
|
+
URL = "url"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ParsedSource:
|
|
37
|
+
"""Container for parsed statement.
|
|
38
|
+
|
|
39
|
+
Holds:
|
|
40
|
+
- source: statement on command content
|
|
41
|
+
- source_type: type of source
|
|
42
|
+
- source_path: in case of URL or FILE path of the origin
|
|
43
|
+
- error: optional message
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("source", "source_type", "source_path", "error")
|
|
47
|
+
__match_args__ = ("source_type", "error")
|
|
48
|
+
|
|
49
|
+
source: io.StringIO
|
|
50
|
+
source_type: SourceType | None
|
|
51
|
+
source_path: str | None
|
|
52
|
+
error: str | None
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
source: str,
|
|
57
|
+
source_type: SourceType,
|
|
58
|
+
source_path: str | None,
|
|
59
|
+
error: str | None = None,
|
|
60
|
+
):
|
|
61
|
+
self.source = io.StringIO(source)
|
|
62
|
+
self.source_type = source_type
|
|
63
|
+
self.source_path = source_path
|
|
64
|
+
self.error = error
|
|
65
|
+
|
|
66
|
+
def __bool__(self):
|
|
67
|
+
return not self.error
|
|
68
|
+
|
|
69
|
+
def __eq__(self, other):
|
|
70
|
+
result = (
|
|
71
|
+
self.source_type == other.source_type,
|
|
72
|
+
self.source_path == other.source_path,
|
|
73
|
+
self.error == other.error,
|
|
74
|
+
self.source.read() == other.source.read(),
|
|
75
|
+
)
|
|
76
|
+
self.source.seek(0)
|
|
77
|
+
other.source.seek(0)
|
|
78
|
+
return all(result)
|
|
79
|
+
|
|
80
|
+
def __repr__(self):
|
|
81
|
+
return f"{self.__class__.__name__}(source_type={self.source_type}, source_path={self.source_path}, error={self.error})"
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_url(cls, path_part: str, raw_source: str) -> "ParsedSource":
|
|
85
|
+
"""Constructor for loading from URL."""
|
|
86
|
+
try:
|
|
87
|
+
payload = urlopen(path_part, timeout=10.0).read().decode()
|
|
88
|
+
return cls(payload, SourceType.URL, path_part)
|
|
89
|
+
|
|
90
|
+
except urllib.error.HTTPError as err:
|
|
91
|
+
error = f"Could not fetch {path_part}: {err}"
|
|
92
|
+
return cls(path_part, SourceType.URL, raw_source, error)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_file(cls, path_part: str, raw_source: str) -> "ParsedSource":
|
|
96
|
+
"""Constructor for loading from file."""
|
|
97
|
+
path = SecurePath(path_part)
|
|
98
|
+
|
|
99
|
+
if path.is_file():
|
|
100
|
+
payload = path.read_text(file_size_limit_mb=UNLIMITED)
|
|
101
|
+
return cls(payload, SourceType.FILE, path.as_posix())
|
|
102
|
+
|
|
103
|
+
error_msg = f"Could not read: {path_part}"
|
|
104
|
+
return cls(path_part, SourceType.FILE, raw_source, error_msg)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
RecursiveStatementReader = Generator[ParsedSource, Any, Any]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_source(source: str, operators: OperatorFunctions) -> ParsedSource:
|
|
111
|
+
"""Evaluates templating and source commands.
|
|
112
|
+
|
|
113
|
+
Returns parsed source according to origin."""
|
|
114
|
+
try:
|
|
115
|
+
statement = source
|
|
116
|
+
for operator in operators:
|
|
117
|
+
statement = operator(statement)
|
|
118
|
+
except UndefinedError as e:
|
|
119
|
+
error_msg = f"SQL template rendering error: {e}"
|
|
120
|
+
return ParsedSource(source, SourceType.UNKNOWN, source, error_msg)
|
|
121
|
+
|
|
122
|
+
split_result = SOURCE_PATTERN.split(statement, maxsplit=1)
|
|
123
|
+
split_result = [p.strip() for p in split_result]
|
|
124
|
+
|
|
125
|
+
if len(split_result) == 1:
|
|
126
|
+
return ParsedSource(statement, SourceType.QUERY, None)
|
|
127
|
+
|
|
128
|
+
_, command, source_path, *_ = split_result
|
|
129
|
+
_path_match = URL_PATTERN.split(source_path.lower())
|
|
130
|
+
|
|
131
|
+
match command.lower(), _path_match:
|
|
132
|
+
# load content from an URL
|
|
133
|
+
case "source" | "load", ("", "http" | "https", *_):
|
|
134
|
+
return ParsedSource.from_url(source_path, statement)
|
|
135
|
+
|
|
136
|
+
# load content from a local file
|
|
137
|
+
case "source" | "load", (str(),):
|
|
138
|
+
return ParsedSource.from_file(source_path, statement)
|
|
139
|
+
|
|
140
|
+
case _:
|
|
141
|
+
error_msg = f"Unknown source: {source_path}"
|
|
142
|
+
|
|
143
|
+
return ParsedSource(source_path, SourceType.UNKNOWN, source, error_msg)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def recursive_source_reader(
|
|
147
|
+
source: SplitedStatements,
|
|
148
|
+
seen_files: list,
|
|
149
|
+
operators: OperatorFunctions,
|
|
150
|
+
remove_comments: bool,
|
|
151
|
+
) -> RecursiveStatementReader:
|
|
152
|
+
"""Based on detected source command reads content of the source and tracks for recursion cycles."""
|
|
153
|
+
for stmt, _ in source:
|
|
154
|
+
if not stmt:
|
|
155
|
+
continue
|
|
156
|
+
parsed_source = parse_source(stmt, operators)
|
|
157
|
+
|
|
158
|
+
match parsed_source:
|
|
159
|
+
case ParsedSource(SourceType.FILE | SourceType.URL, None):
|
|
160
|
+
if parsed_source.source_path in seen_files:
|
|
161
|
+
error = f"Recursion detected: {' -> '.join(seen_files)}"
|
|
162
|
+
parsed_source.error = error
|
|
163
|
+
yield parsed_source
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
seen_files.append(parsed_source.source_path)
|
|
167
|
+
|
|
168
|
+
yield from recursive_source_reader(
|
|
169
|
+
split_statements(parsed_source.source, remove_comments),
|
|
170
|
+
seen_files,
|
|
171
|
+
operators,
|
|
172
|
+
remove_comments,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
seen_files.pop()
|
|
176
|
+
|
|
177
|
+
case ParsedSource(SourceType.URL, error) if error:
|
|
178
|
+
yield parsed_source
|
|
179
|
+
|
|
180
|
+
case _:
|
|
181
|
+
yield parsed_source
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def files_reader(
|
|
186
|
+
paths: Sequence[SecurePath],
|
|
187
|
+
operators: OperatorFunctions,
|
|
188
|
+
remove_comments: bool = False,
|
|
189
|
+
) -> RecursiveStatementReader:
|
|
190
|
+
"""Entry point for reading statements from files.
|
|
191
|
+
|
|
192
|
+
Returns a generator with statements."""
|
|
193
|
+
for path in paths:
|
|
194
|
+
with path.open(read_file_limit_mb=UNLIMITED) as f:
|
|
195
|
+
stmts = split_statements(io.StringIO(f.read()), remove_comments)
|
|
196
|
+
yield from recursive_source_reader(
|
|
197
|
+
stmts,
|
|
198
|
+
[path.as_posix()],
|
|
199
|
+
operators,
|
|
200
|
+
remove_comments,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def query_reader(
|
|
205
|
+
source: str,
|
|
206
|
+
operators: OperatorFunctions,
|
|
207
|
+
remove_comments: bool = False,
|
|
208
|
+
) -> RecursiveStatementReader:
|
|
209
|
+
"""Entry point for reading statements from query.
|
|
210
|
+
|
|
211
|
+
Returns a generator with statements."""
|
|
212
|
+
stmts = split_statements(io.StringIO(source), remove_comments)
|
|
213
|
+
yield from recursive_source_reader(stmts, [], operators, remove_comments)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def compile_statements(source: RecursiveStatementReader):
|
|
217
|
+
"""Tracks statements evaluation and collects errors."""
|
|
218
|
+
errors = []
|
|
219
|
+
cnt = 0
|
|
220
|
+
compiled = []
|
|
221
|
+
|
|
222
|
+
for stmt in source:
|
|
223
|
+
if stmt.source_type == SourceType.QUERY:
|
|
224
|
+
cnt += 1
|
|
225
|
+
if not stmt.error:
|
|
226
|
+
compiled.append(stmt.source.read())
|
|
227
|
+
if stmt.error:
|
|
228
|
+
errors.append(stmt.error)
|
|
229
|
+
|
|
230
|
+
return errors, cnt, compiled
|
|
@@ -439,9 +439,13 @@ class StageManager(SqlExecutionMixin):
|
|
|
439
439
|
# We end if we reach the root directory
|
|
440
440
|
if directory == temp_dir_with_copy:
|
|
441
441
|
break
|
|
442
|
-
|
|
443
442
|
# Add parent directory to the list if it's not already there
|
|
444
|
-
if directory.parent not in deepest_dirs_list
|
|
443
|
+
if directory.parent not in deepest_dirs_list and not any(
|
|
444
|
+
(
|
|
445
|
+
existing_dir.is_relative_to(directory.parent)
|
|
446
|
+
for existing_dir in deepest_dirs_list
|
|
447
|
+
)
|
|
448
|
+
):
|
|
445
449
|
deepest_dirs_list.append(directory.parent)
|
|
446
450
|
|
|
447
451
|
# Remove the directory so the parent directory will contain only files
|
|
@@ -561,7 +565,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
561
565
|
)
|
|
562
566
|
|
|
563
567
|
parsed_variables = parse_key_value_variables(variables)
|
|
564
|
-
sql_variables = self.
|
|
568
|
+
sql_variables = self.parse_execute_variables(parsed_variables)
|
|
565
569
|
python_variables = self._parse_python_variables(parsed_variables)
|
|
566
570
|
results = []
|
|
567
571
|
|
|
@@ -663,7 +667,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
663
667
|
return [f for f in files if Path(f).suffix in EXECUTE_SUPPORTED_FILES_FORMATS]
|
|
664
668
|
|
|
665
669
|
@staticmethod
|
|
666
|
-
def
|
|
670
|
+
def parse_execute_variables(variables: List[Variable]) -> Optional[str]:
|
|
667
671
|
if not variables:
|
|
668
672
|
return None
|
|
669
673
|
query_parameters = [f"{v.key}=>{v.value}" for v in variables]
|
|
@@ -703,6 +707,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
703
707
|
original_file: str,
|
|
704
708
|
) -> Dict:
|
|
705
709
|
try:
|
|
710
|
+
log.info("Executing SQL file: %s", file_stage_path)
|
|
706
711
|
query = f"execute immediate from {self.quote_stage_name(file_stage_path)}"
|
|
707
712
|
if variables:
|
|
708
713
|
query += variables
|
|
@@ -816,6 +821,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
816
821
|
from snowflake.snowpark.exceptions import SnowparkSQLException
|
|
817
822
|
|
|
818
823
|
try:
|
|
824
|
+
log.info("Executing Python file: %s", file_stage_path)
|
|
819
825
|
self._python_exe_procedure(self.get_standard_stage_prefix(file_stage_path), variables, session=self.snowpark_session) # type: ignore
|
|
820
826
|
return StageManager._success_result(file=original_file)
|
|
821
827
|
except SnowparkSQLException as e:
|
|
@@ -16,11 +16,10 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Dict
|
|
20
19
|
|
|
21
20
|
import click
|
|
22
21
|
import typer
|
|
23
|
-
from click import ClickException
|
|
22
|
+
from click import ClickException
|
|
24
23
|
from snowflake.cli._plugins.object.command_aliases import (
|
|
25
24
|
add_object_command_aliases,
|
|
26
25
|
scope_option,
|
|
@@ -44,6 +43,7 @@ from snowflake.cli.api.commands.flags import (
|
|
|
44
43
|
like_option,
|
|
45
44
|
)
|
|
46
45
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
46
|
+
from snowflake.cli.api.commands.utils import get_entity_for_operation
|
|
47
47
|
from snowflake.cli.api.constants import ObjectType
|
|
48
48
|
from snowflake.cli.api.exceptions import NoProjectDefinitionError
|
|
49
49
|
from snowflake.cli.api.identifiers import FQN
|
|
@@ -133,7 +133,8 @@ def _default_file_callback(param_name: str):
|
|
|
133
133
|
@with_experimental_behaviour()
|
|
134
134
|
def streamlit_deploy(
|
|
135
135
|
replace: bool = ReplaceOption(
|
|
136
|
-
help="
|
|
136
|
+
help="Replaces the Streamlit app if it already exists. It only uploads new and overwrites existing files, "
|
|
137
|
+
"but does not remove any files already on the stage."
|
|
137
138
|
),
|
|
138
139
|
entity_id: str = entity_argument("streamlit"),
|
|
139
140
|
open_: bool = OpenOption,
|
|
@@ -155,30 +156,14 @@ def streamlit_deploy(
|
|
|
155
156
|
)
|
|
156
157
|
pd = convert_project_definition_to_v2(cli_context.project_root, pd)
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
streamlit: StreamlitEntityModel = get_entity_for_operation(
|
|
160
|
+
cli_context=cli_context,
|
|
161
|
+
entity_id=entity_id,
|
|
162
|
+
project_definition=pd,
|
|
163
|
+
entity_type="streamlit",
|
|
160
164
|
)
|
|
161
165
|
|
|
162
166
|
streamlit_project_paths = StreamlitProjectPaths(cli_context.project_root)
|
|
163
|
-
|
|
164
|
-
if not streamlits:
|
|
165
|
-
raise NoProjectDefinitionError(
|
|
166
|
-
project_type="streamlit", project_root=cli_context.project_root
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
if entity_id and entity_id not in streamlits:
|
|
170
|
-
raise UsageError(f"No '{entity_id}' entity in project definition file.")
|
|
171
|
-
|
|
172
|
-
if len(streamlits.keys()) == 1:
|
|
173
|
-
entity_id = list(streamlits.keys())[0]
|
|
174
|
-
|
|
175
|
-
if entity_id is None:
|
|
176
|
-
raise UsageError(
|
|
177
|
-
"Multiple Streamlit apps found. Please provide entity id for the operation."
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
# Get first streamlit
|
|
181
|
-
streamlit: StreamlitEntityModel = streamlits[entity_id]
|
|
182
167
|
url = StreamlitManager().deploy(
|
|
183
168
|
streamlit=streamlit,
|
|
184
169
|
streamlit_project_paths=streamlit_project_paths,
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
-
from pathlib import PurePosixPath
|
|
19
18
|
from typing import List, Optional
|
|
20
19
|
|
|
21
20
|
from click import ClickException
|
|
@@ -32,8 +31,7 @@ from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
|
32
31
|
from snowflake.cli._plugins.streamlit.streamlit_project_paths import (
|
|
33
32
|
StreamlitProjectPaths,
|
|
34
33
|
)
|
|
35
|
-
from snowflake.cli.api.artifacts.
|
|
36
|
-
from snowflake.cli.api.artifacts.utils import symlink_or_copy
|
|
34
|
+
from snowflake.cli.api.artifacts.upload import put_files
|
|
37
35
|
from snowflake.cli.api.commands.experimental_behaviour import (
|
|
38
36
|
experimental_behaviour_enabled,
|
|
39
37
|
)
|
|
@@ -65,40 +63,11 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
65
63
|
artifacts: Optional[List[PathMapping]] = None,
|
|
66
64
|
):
|
|
67
65
|
cli_console.step(f"Deploying files to {stage_root}")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
bundle_map = BundleMap(
|
|
73
|
-
project_root=streamlit_project_paths.project_root,
|
|
74
|
-
deploy_root=streamlit_project_paths.bundle_root,
|
|
66
|
+
put_files(
|
|
67
|
+
project_paths=streamlit_project_paths,
|
|
68
|
+
stage_root=stage_root,
|
|
69
|
+
artifacts=artifacts,
|
|
75
70
|
)
|
|
76
|
-
for artifact in artifacts:
|
|
77
|
-
bundle_map.add(PathMapping(src=str(artifact.src), dest=artifact.dest))
|
|
78
|
-
|
|
79
|
-
# Clean up bundle root
|
|
80
|
-
streamlit_project_paths.remove_up_bundle_root()
|
|
81
|
-
|
|
82
|
-
for (absolute_src, absolute_dest) in bundle_map.all_mappings(
|
|
83
|
-
absolute=True, expand_directories=True
|
|
84
|
-
):
|
|
85
|
-
if absolute_src.is_file():
|
|
86
|
-
# We treat the bundle/streamlit root as deploy root
|
|
87
|
-
symlink_or_copy(
|
|
88
|
-
absolute_src,
|
|
89
|
-
absolute_dest,
|
|
90
|
-
deploy_root=streamlit_project_paths.bundle_root,
|
|
91
|
-
)
|
|
92
|
-
# Temporary solution, will be replaced with diff
|
|
93
|
-
stage_path = (
|
|
94
|
-
PurePosixPath(absolute_dest)
|
|
95
|
-
.relative_to(streamlit_project_paths.bundle_root)
|
|
96
|
-
.parent
|
|
97
|
-
)
|
|
98
|
-
full_stage_path = f"{stage_root}/{stage_path}".rstrip("/")
|
|
99
|
-
stage_manager.put(
|
|
100
|
-
local_path=absolute_dest, stage_path=full_stage_path, overwrite=True
|
|
101
|
-
)
|
|
102
71
|
|
|
103
72
|
def _create_streamlit(
|
|
104
73
|
self,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pathlib import PurePosixPath
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from snowflake.cli._plugins.stage.manager import StageManager
|
|
5
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
6
|
+
from snowflake.cli.api.artifacts.utils import symlink_or_copy
|
|
7
|
+
from snowflake.cli.api.console import cli_console
|
|
8
|
+
from snowflake.cli.api.project.project_paths import ProjectPaths
|
|
9
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def put_files(
|
|
13
|
+
project_paths: ProjectPaths,
|
|
14
|
+
stage_root: str,
|
|
15
|
+
artifacts: Optional[List[PathMapping]] = None,
|
|
16
|
+
):
|
|
17
|
+
if not artifacts:
|
|
18
|
+
return
|
|
19
|
+
stage_manager = StageManager()
|
|
20
|
+
# We treat the bundle root as deploy root
|
|
21
|
+
bundle_map = BundleMap(
|
|
22
|
+
project_root=project_paths.project_root,
|
|
23
|
+
deploy_root=project_paths.bundle_root,
|
|
24
|
+
)
|
|
25
|
+
for artifact in artifacts:
|
|
26
|
+
bundle_map.add(PathMapping(src=str(artifact.src), dest=artifact.dest))
|
|
27
|
+
|
|
28
|
+
# Clean up bundle root
|
|
29
|
+
project_paths.remove_up_bundle_root()
|
|
30
|
+
|
|
31
|
+
for (absolute_src, absolute_dest) in bundle_map.all_mappings(
|
|
32
|
+
absolute=True, expand_directories=True
|
|
33
|
+
):
|
|
34
|
+
if absolute_src.is_file():
|
|
35
|
+
# We treat the bundle/streamlit root as deploy root
|
|
36
|
+
symlink_or_copy(
|
|
37
|
+
absolute_src,
|
|
38
|
+
absolute_dest,
|
|
39
|
+
deploy_root=project_paths.bundle_root,
|
|
40
|
+
)
|
|
41
|
+
# Temporary solution, will be replaced with diff
|
|
42
|
+
stage_path = (
|
|
43
|
+
PurePosixPath(absolute_dest)
|
|
44
|
+
.relative_to(project_paths.bundle_root)
|
|
45
|
+
.parent
|
|
46
|
+
)
|
|
47
|
+
full_stage_path = f"{stage_root}/{stage_path}".rstrip("/")
|
|
48
|
+
cli_console.step(f"Uploading {absolute_dest} to {full_stage_path}")
|
|
49
|
+
stage_manager.put(
|
|
50
|
+
local_path=absolute_dest, stage_path=full_stage_path, overwrite=True
|
|
51
|
+
)
|