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.
Files changed (73) hide show
  1. snowflake/cli/__about__.py +13 -1
  2. snowflake/cli/_app/cli_app.py +1 -10
  3. snowflake/cli/_app/commands_registration/builtin_plugins.py +7 -1
  4. snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
  5. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +3 -3
  6. snowflake/cli/_app/printing.py +2 -2
  7. snowflake/cli/_app/snow_connector.py +5 -4
  8. snowflake/cli/_app/telemetry.py +3 -15
  9. snowflake/cli/_app/version_check.py +4 -4
  10. snowflake/cli/_plugins/auth/__init__.py +11 -0
  11. snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
  12. snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
  13. snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
  14. snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
  15. snowflake/cli/_plugins/connection/commands.py +79 -5
  16. snowflake/cli/_plugins/helpers/commands.py +3 -4
  17. snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
  19. snowflake/cli/_plugins/notebook/commands.py +3 -4
  20. snowflake/cli/_plugins/object/command_aliases.py +3 -1
  21. snowflake/cli/_plugins/object/manager.py +4 -2
  22. snowflake/cli/_plugins/plugin/commands.py +79 -0
  23. snowflake/cli/_plugins/plugin/manager.py +74 -0
  24. snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
  25. snowflake/cli/_plugins/project/__init__.py +0 -0
  26. snowflake/cli/_plugins/project/commands.py +173 -0
  27. snowflake/cli/{_app/api_impl/plugin/__init__.py → _plugins/project/feature_flags.py} +9 -0
  28. snowflake/cli/_plugins/project/manager.py +76 -0
  29. snowflake/cli/_plugins/project/plugin_spec.py +30 -0
  30. snowflake/cli/_plugins/project/project_entity_model.py +40 -0
  31. snowflake/cli/_plugins/snowpark/commands.py +2 -1
  32. snowflake/cli/_plugins/spcs/compute_pool/commands.py +70 -10
  33. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
  34. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
  35. snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
  36. snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
  37. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
  38. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
  39. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  40. snowflake/cli/_plugins/spcs/services/commands.py +53 -0
  41. snowflake/cli/_plugins/spcs/services/manager.py +114 -0
  42. snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
  43. snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
  44. snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
  45. snowflake/cli/_plugins/sql/manager.py +42 -51
  46. snowflake/cli/_plugins/sql/source_reader.py +230 -0
  47. snowflake/cli/_plugins/stage/manager.py +10 -4
  48. snowflake/cli/_plugins/streamlit/commands.py +9 -24
  49. snowflake/cli/_plugins/streamlit/manager.py +5 -36
  50. snowflake/cli/api/artifacts/upload.py +51 -0
  51. snowflake/cli/api/commands/flags.py +35 -10
  52. snowflake/cli/api/commands/snow_typer.py +12 -0
  53. snowflake/cli/api/commands/utils.py +2 -0
  54. snowflake/cli/api/config.py +15 -10
  55. snowflake/cli/api/constants.py +2 -0
  56. snowflake/cli/api/errno.py +1 -0
  57. snowflake/cli/api/exceptions.py +15 -1
  58. snowflake/cli/api/feature_flags.py +2 -0
  59. snowflake/cli/api/plugins/plugin_config.py +43 -4
  60. snowflake/cli/api/project/definition_helper.py +31 -0
  61. snowflake/cli/api/project/schemas/entities/entities.py +26 -0
  62. snowflake/cli/api/rest_api.py +2 -3
  63. snowflake/cli/{_app → api}/secret.py +4 -1
  64. snowflake/cli/api/secure_path.py +16 -4
  65. snowflake/cli/api/sql_execution.py +7 -3
  66. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +12 -12
  67. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +71 -50
  68. snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
  69. snowflake/cli/api/__init__.py +0 -48
  70. /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
  71. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
  72. {snowflake_cli-3.4.1.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
  73. {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 io import StringIO
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 UNLIMITED, SecurePath
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
- inputs = [query, files, std_in]
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
- if std_in:
52
- query = sys.stdin.read()
53
- if query:
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
- if files:
59
- # Multiple files
60
- results = []
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
- # Use single_statement if there's only one, otherwise this is multi statement result
72
- single_statement = len(files) == 1 and single_statement
73
- return single_statement, chain.from_iterable(results)
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
- # At that point, no stdin, query or files were provided
76
- raise UsageError("Use either query, filename or input option.")
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
- def _execute_single_query(
79
- self, query: str, data: Dict | None = None, retain_comments: bool = False
80
- ) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
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
- statements = tuple(
88
- statement
89
- for statement, _ in split_statements(
90
- StringIO(query), remove_comments=not retain_comments
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
- return single_statement, self._execute_string(
96
- "\n".join(statements), cursor_class=VerboseCursor
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._parse_execute_variables(parsed_variables)
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 _parse_execute_variables(variables: List[Variable]) -> Optional[str]:
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, UsageError
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="Replace the Streamlit app if it already exists."
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
- streamlits: Dict[str, StreamlitEntityModel] = pd.get_entities_by_type(
159
- entity_type="streamlit"
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.bundle_map import BundleMap
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
- if not artifacts:
69
- return
70
- stage_manager = StageManager()
71
- # We treat the bundle root as deploy root
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
+ )