snowflake-cli-labs 2.3.0rc1__py3-none-any.whl → 2.4.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/api/__init__.py +2 -0
- snowflake/cli/api/cli_global_context.py +8 -1
- snowflake/cli/api/commands/decorators.py +2 -2
- snowflake/cli/api/commands/flags.py +49 -4
- snowflake/cli/api/commands/snow_typer.py +2 -0
- snowflake/cli/api/console/abc.py +2 -0
- snowflake/cli/api/console/console.py +6 -5
- snowflake/cli/api/constants.py +5 -0
- snowflake/cli/api/exceptions.py +12 -0
- snowflake/cli/api/identifiers.py +123 -0
- snowflake/cli/api/plugins/command/__init__.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +2 -0
- snowflake/cli/api/project/definition.py +2 -0
- snowflake/cli/api/project/errors.py +3 -3
- snowflake/cli/api/project/schemas/identifier_model.py +35 -0
- snowflake/cli/api/project/schemas/native_app/native_app.py +4 -0
- snowflake/cli/api/project/schemas/native_app/path_mapping.py +21 -3
- snowflake/cli/api/project/schemas/project_definition.py +58 -6
- snowflake/cli/api/project/schemas/snowpark/argument.py +2 -0
- snowflake/cli/api/project/schemas/snowpark/callable.py +8 -17
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +2 -2
- snowflake/cli/api/project/schemas/updatable_model.py +2 -0
- snowflake/cli/api/project/util.py +2 -0
- snowflake/cli/api/secure_path.py +2 -0
- snowflake/cli/api/sql_execution.py +14 -54
- snowflake/cli/api/utils/cursor.py +2 -0
- snowflake/cli/api/utils/models.py +23 -0
- snowflake/cli/api/utils/naming_utils.py +0 -27
- snowflake/cli/api/utils/rendering.py +178 -23
- snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +2 -0
- snowflake/cli/app/cli_app.py +4 -1
- snowflake/cli/app/commands_registration/builtin_plugins.py +8 -0
- snowflake/cli/app/commands_registration/command_plugins_loader.py +2 -0
- snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +2 -0
- snowflake/cli/app/commands_registration/typer_registration.py +2 -0
- snowflake/cli/app/dev/pycharm_remote_debug.py +2 -0
- snowflake/cli/app/loggers.py +2 -0
- snowflake/cli/app/main_typer.py +1 -1
- snowflake/cli/app/printing.py +3 -1
- snowflake/cli/app/snow_connector.py +2 -2
- snowflake/cli/plugins/connection/commands.py +5 -14
- snowflake/cli/plugins/connection/util.py +1 -1
- snowflake/cli/plugins/cortex/__init__.py +0 -0
- snowflake/cli/plugins/cortex/commands.py +312 -0
- snowflake/cli/plugins/cortex/constants.py +3 -0
- snowflake/cli/plugins/cortex/manager.py +175 -0
- snowflake/cli/plugins/cortex/plugin_spec.py +16 -0
- snowflake/cli/plugins/cortex/types.py +8 -0
- snowflake/cli/plugins/git/commands.py +15 -0
- snowflake/cli/plugins/nativeapp/artifacts.py +368 -123
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +45 -0
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +104 -0
- snowflake/cli/plugins/nativeapp/codegen/sandbox.py +2 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +181 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +196 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +47 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +489 -0
- snowflake/cli/plugins/nativeapp/commands.py +11 -4
- snowflake/cli/plugins/nativeapp/common_flags.py +12 -5
- snowflake/cli/plugins/nativeapp/constants.py +1 -0
- snowflake/cli/plugins/nativeapp/manager.py +49 -16
- snowflake/cli/plugins/nativeapp/policy.py +2 -0
- snowflake/cli/plugins/nativeapp/run_processor.py +28 -10
- snowflake/cli/plugins/nativeapp/teardown_processor.py +80 -8
- snowflake/cli/plugins/nativeapp/utils.py +7 -6
- snowflake/cli/plugins/nativeapp/version/commands.py +6 -5
- snowflake/cli/plugins/nativeapp/version/version_processor.py +2 -0
- snowflake/cli/plugins/notebook/commands.py +21 -0
- snowflake/cli/plugins/notebook/exceptions.py +6 -0
- snowflake/cli/plugins/notebook/manager.py +46 -3
- snowflake/cli/plugins/notebook/types.py +2 -0
- snowflake/cli/plugins/object/command_aliases.py +80 -0
- snowflake/cli/plugins/object/commands.py +10 -6
- snowflake/cli/plugins/object/common.py +2 -0
- snowflake/cli/plugins/object_stage_deprecated/__init__.py +1 -0
- snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +20 -0
- snowflake/cli/plugins/snowpark/commands.py +62 -6
- snowflake/cli/plugins/snowpark/common.py +17 -6
- snowflake/cli/plugins/spcs/compute_pool/commands.py +22 -1
- snowflake/cli/plugins/spcs/compute_pool/manager.py +2 -0
- snowflake/cli/plugins/spcs/image_repository/commands.py +25 -1
- snowflake/cli/plugins/spcs/image_repository/manager.py +3 -1
- snowflake/cli/plugins/spcs/services/commands.py +39 -5
- snowflake/cli/plugins/spcs/services/manager.py +2 -0
- snowflake/cli/plugins/sql/commands.py +13 -5
- snowflake/cli/plugins/sql/manager.py +40 -19
- snowflake/cli/plugins/stage/commands.py +29 -3
- snowflake/cli/plugins/stage/diff.py +2 -0
- snowflake/cli/plugins/streamlit/commands.py +26 -10
- snowflake/cli/plugins/streamlit/manager.py +9 -10
- {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/METADATA +4 -2
- {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/RECORD +97 -77
- /snowflake/cli/plugins/{object/stage_deprecated → object_stage_deprecated}/commands.py +0 -0
- {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
1
4
|
import os
|
|
2
5
|
from dataclasses import dataclass
|
|
3
6
|
from pathlib import Path
|
|
4
|
-
from
|
|
7
|
+
from textwrap import dedent
|
|
8
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
5
9
|
|
|
6
10
|
from click.exceptions import ClickException
|
|
7
11
|
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
|
|
@@ -9,9 +13,6 @@ from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMappin
|
|
|
9
13
|
from snowflake.cli.api.secure_path import SecurePath
|
|
10
14
|
from yaml import safe_load
|
|
11
15
|
|
|
12
|
-
# Map from source directories and files in the project directory to their path in the deploy directory. Both paths are absolute.
|
|
13
|
-
ArtifactDeploymentMap = Dict[Path, Path]
|
|
14
|
-
|
|
15
16
|
|
|
16
17
|
class DeployRootError(ClickException):
|
|
17
18
|
"""
|
|
@@ -31,26 +32,13 @@ class ArtifactError(ClickException):
|
|
|
31
32
|
super().__init__(msg)
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class GlobMatchedNothingError(ClickException):
|
|
35
|
-
"""
|
|
36
|
-
No files were found that matched the provided glob pattern.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, src: str):
|
|
40
|
-
super().__init__(f"{self.__doc__}: {src}")
|
|
41
|
-
|
|
42
|
-
|
|
43
35
|
class SourceNotFoundError(ClickException):
|
|
44
36
|
"""
|
|
45
|
-
|
|
46
|
-
in the project directory.
|
|
37
|
+
No match was found for the specified source in the project directory
|
|
47
38
|
"""
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def __init__(self, path: Path):
|
|
52
|
-
super().__init__(f"{self.__doc__}\npath = {path}")
|
|
53
|
-
self.path = path
|
|
40
|
+
def __init__(self, src: Union[str, Path]):
|
|
41
|
+
super().__init__(f"{dedent(str(self.__doc__))}: {src}".strip())
|
|
54
42
|
|
|
55
43
|
|
|
56
44
|
class TooManyFilesError(ClickException):
|
|
@@ -61,7 +49,9 @@ class TooManyFilesError(ClickException):
|
|
|
61
49
|
dest_path: Path
|
|
62
50
|
|
|
63
51
|
def __init__(self, dest_path: Path):
|
|
64
|
-
super().__init__(
|
|
52
|
+
super().__init__(
|
|
53
|
+
f"{dedent(str(self.__doc__))}\ndestination = {dest_path}".strip()
|
|
54
|
+
)
|
|
65
55
|
self.dest_path = dest_path
|
|
66
56
|
|
|
67
57
|
|
|
@@ -73,37 +63,317 @@ class NotInDeployRootError(ClickException):
|
|
|
73
63
|
(use "./" instead to copy into the deploy root).
|
|
74
64
|
"""
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
dest_path: Path
|
|
66
|
+
dest_path: Union[str, Path]
|
|
78
67
|
deploy_root: Path
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
68
|
+
src_path: Optional[Union[str, Path]]
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
dest_path: Union[Path, str],
|
|
74
|
+
deploy_root: Path,
|
|
75
|
+
src_path: Optional[Union[str, Path]] = None,
|
|
76
|
+
):
|
|
77
|
+
message = dedent(str(self.__doc__))
|
|
78
|
+
message += f"\ndestination = {dest_path}"
|
|
79
|
+
message += f"\ndeploy root = {deploy_root}"
|
|
80
|
+
if src_path is not None:
|
|
81
|
+
message += f"""\nsource = {src_path}"""
|
|
82
|
+
super().__init__(message.strip())
|
|
90
83
|
self.dest_path = dest_path
|
|
91
84
|
self.deploy_root = deploy_root
|
|
85
|
+
self.src_path = src_path
|
|
92
86
|
|
|
93
87
|
|
|
94
88
|
@dataclass
|
|
95
89
|
class ArtifactMapping:
|
|
96
90
|
"""
|
|
97
|
-
Used to keep track of equivalent paths / globs so we can copy
|
|
98
|
-
artifacts from the project folder to the deploy root.
|
|
91
|
+
Used to keep track of equivalent paths / globs so we can copy artifacts from the project folder to the deploy root.
|
|
99
92
|
"""
|
|
100
93
|
|
|
101
94
|
src: str
|
|
102
95
|
dest: str
|
|
103
96
|
|
|
104
97
|
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
ArtifactPredicate = Callable[[Path, Path], bool]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BundleMap:
|
|
102
|
+
"""
|
|
103
|
+
Computes the mapping between project directory artifacts (aka source artifacts) to their deploy root location
|
|
104
|
+
(aka destination artifact). This information is primarily used when bundling a native applications project.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, *, project_root: Path, deploy_root: Path):
|
|
108
|
+
self._project_root: Path = resolve_without_follow(project_root)
|
|
109
|
+
self._deploy_root: Path = resolve_without_follow(deploy_root)
|
|
110
|
+
self._src_to_dest: Dict[Path, List[Path]] = {}
|
|
111
|
+
self._dest_to_src: Dict[Path, List[Path]] = {}
|
|
112
|
+
self._dest_is_dir: Dict[Path, bool] = {}
|
|
113
|
+
|
|
114
|
+
def deploy_root(self) -> Path:
|
|
115
|
+
return self._deploy_root
|
|
116
|
+
|
|
117
|
+
def project_root(self) -> Path:
|
|
118
|
+
return self._project_root
|
|
119
|
+
|
|
120
|
+
def _add(self, src: Path, dest: Path, map_as_child: bool) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Adds the specified artifact mapping rule to this map.
|
|
123
|
+
|
|
124
|
+
Arguments:
|
|
125
|
+
src {Path} -- the source path
|
|
126
|
+
dest {Path} -- the destination path
|
|
127
|
+
map_as_child {bool} -- when True, the source will be added as a child of the specified destination.
|
|
128
|
+
"""
|
|
129
|
+
absolute_src = self._absolute_src(src)
|
|
130
|
+
absolute_dest = self._absolute_dest(dest, src_path=src)
|
|
131
|
+
dest_is_dir = absolute_src.is_dir() or map_as_child
|
|
132
|
+
|
|
133
|
+
# Check for the special case of './' as a target ('.' is not allowed)
|
|
134
|
+
if absolute_dest == self._deploy_root and not map_as_child:
|
|
135
|
+
raise NotInDeployRootError(
|
|
136
|
+
dest_path=dest, deploy_root=self._deploy_root, src_path=src
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if self._deploy_root in absolute_src.parents:
|
|
140
|
+
# ignore this item since it's in the deploy root. This can happen if the bundle map is create
|
|
141
|
+
# after the bundle step and a project is using rules that are not sufficiently constrained.
|
|
142
|
+
# Since the bundle step starts with deleting the deploy root, we wouldn't normally encounter this situation.
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
canonical_src = self._canonical_src(src)
|
|
146
|
+
canonical_dest = self._canonical_dest(dest)
|
|
147
|
+
|
|
148
|
+
src_is_dir = absolute_src.is_dir()
|
|
149
|
+
if map_as_child:
|
|
150
|
+
# Make sure the destination is a child of the original, since this was requested
|
|
151
|
+
canonical_dest = canonical_dest / canonical_src.name
|
|
152
|
+
dest_is_dir = src.is_dir()
|
|
153
|
+
|
|
154
|
+
# Verify that multiple files are not being mapped to a single file destination
|
|
155
|
+
current_sources = self._dest_to_src.setdefault(canonical_dest, [])
|
|
156
|
+
if not dest_is_dir:
|
|
157
|
+
# the destination is a file
|
|
158
|
+
if (canonical_src not in current_sources) and len(current_sources) > 0:
|
|
159
|
+
raise TooManyFilesError(dest)
|
|
160
|
+
|
|
161
|
+
# Perform all updates together we don't end up with inconsistent state
|
|
162
|
+
self._update_dest_is_dir(canonical_dest, dest_is_dir)
|
|
163
|
+
current_dests = self._src_to_dest.setdefault(canonical_src, [])
|
|
164
|
+
if canonical_dest not in current_dests:
|
|
165
|
+
current_dests.append(canonical_dest)
|
|
166
|
+
if canonical_src not in current_sources:
|
|
167
|
+
current_sources.append(canonical_src)
|
|
168
|
+
|
|
169
|
+
def _add_mapping(self, src: str, dest: Optional[str]):
|
|
170
|
+
"""
|
|
171
|
+
Adds the specified artifact rule to this instance. The source should be relative to the project directory. It
|
|
172
|
+
is interpreted as a file, directory or glob pattern. If the destination path is not specified, each source match
|
|
173
|
+
is mapped to an identical path in the deploy root.
|
|
174
|
+
"""
|
|
175
|
+
match_found = False
|
|
176
|
+
|
|
177
|
+
src_path = Path(src)
|
|
178
|
+
if src_path.is_absolute():
|
|
179
|
+
raise ArtifactError("Source path must be a relative path")
|
|
180
|
+
|
|
181
|
+
for resolved_src in self._project_root.glob(src):
|
|
182
|
+
match_found = True
|
|
183
|
+
|
|
184
|
+
if dest:
|
|
185
|
+
dest_stem = dest.rstrip("/")
|
|
186
|
+
if not dest_stem:
|
|
187
|
+
# handle '/' as the destination as a special case. This is because specifying only '/' as a
|
|
188
|
+
# a destination looks like '.' once all forwards slashes are stripped. If we don't handle it
|
|
189
|
+
# specially here, `dest: /` would incorrectly be allowed.
|
|
190
|
+
raise NotInDeployRootError(
|
|
191
|
+
dest_path=dest,
|
|
192
|
+
deploy_root=self._deploy_root,
|
|
193
|
+
src_path=resolved_src,
|
|
194
|
+
)
|
|
195
|
+
dest_path = Path(dest.rstrip("/"))
|
|
196
|
+
if dest_path.is_absolute():
|
|
197
|
+
raise ArtifactError("Destination path must be a relative path")
|
|
198
|
+
self._add(resolved_src, dest_path, specifies_directory(dest))
|
|
199
|
+
else:
|
|
200
|
+
self._add(
|
|
201
|
+
resolved_src,
|
|
202
|
+
resolved_src.relative_to(self._project_root),
|
|
203
|
+
False,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if not match_found:
|
|
207
|
+
raise SourceNotFoundError(src)
|
|
208
|
+
|
|
209
|
+
def add(self, mapping: Union[ArtifactMapping, PathMapping]) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Adds an artifact mapping rule to this instance.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(mapping, ArtifactMapping):
|
|
214
|
+
self._add_mapping(mapping.src, mapping.dest)
|
|
215
|
+
elif isinstance(mapping, PathMapping):
|
|
216
|
+
self._add_mapping(mapping.src, mapping.dest)
|
|
217
|
+
else:
|
|
218
|
+
raise RuntimeError(f"Unsupported mapping type: {type(mapping)}")
|
|
219
|
+
|
|
220
|
+
def _mappings_for_source(
|
|
221
|
+
self,
|
|
222
|
+
src: Path,
|
|
223
|
+
absolute: bool = False,
|
|
224
|
+
expand_directories: bool = False,
|
|
225
|
+
predicate: ArtifactPredicate = lambda src, dest: True,
|
|
226
|
+
) -> Iterator[Tuple[Path, Path]]:
|
|
227
|
+
canonical_src = self._canonical_src(src)
|
|
228
|
+
canonical_dests = self._src_to_dest.get(canonical_src)
|
|
229
|
+
assert canonical_dests is not None
|
|
230
|
+
|
|
231
|
+
absolute_src = self._absolute_src(canonical_src)
|
|
232
|
+
src_for_output = self._to_output_src(absolute_src, absolute)
|
|
233
|
+
dests_for_output = [self._to_output_dest(p, absolute) for p in canonical_dests]
|
|
234
|
+
|
|
235
|
+
for d in dests_for_output:
|
|
236
|
+
if predicate(src_for_output, d):
|
|
237
|
+
yield src_for_output, d
|
|
238
|
+
|
|
239
|
+
if absolute_src.is_dir() and expand_directories:
|
|
240
|
+
# both src and dest are directories, and expanding directories was requested. Traverse src, and map each
|
|
241
|
+
# file to the dest directory
|
|
242
|
+
for (root, subdirs, files) in os.walk(absolute_src, followlinks=True):
|
|
243
|
+
relative_root = Path(root).relative_to(absolute_src)
|
|
244
|
+
for name in itertools.chain(subdirs, files):
|
|
245
|
+
for d in dests_for_output:
|
|
246
|
+
src_file_for_output = src_for_output / relative_root / name
|
|
247
|
+
dest_file_for_output = d / relative_root / name
|
|
248
|
+
if predicate(src_file_for_output, dest_file_for_output):
|
|
249
|
+
yield src_file_for_output, dest_file_for_output
|
|
250
|
+
|
|
251
|
+
def all_mappings(
|
|
252
|
+
self,
|
|
253
|
+
absolute: bool = False,
|
|
254
|
+
expand_directories: bool = False,
|
|
255
|
+
predicate: ArtifactPredicate = lambda src, dest: True,
|
|
256
|
+
) -> Iterator[Tuple[Path, Path]]:
|
|
257
|
+
"""
|
|
258
|
+
Yields a (src, dest) pair for each deployed artifact in the project. Each pair corresponds to a single file
|
|
259
|
+
in the project. Source directories are resolved as needed to resolve their contents.
|
|
260
|
+
|
|
261
|
+
Arguments:
|
|
262
|
+
self: this instance
|
|
263
|
+
absolute (bool): Specifies whether the yielded paths should be joined with the project or deploy roots,
|
|
264
|
+
as appropriate.
|
|
265
|
+
expand_directories (bool): Specifies whether directory to directory mappings should be expanded to
|
|
266
|
+
resolve their contained files.
|
|
267
|
+
predicate (PathPredicate): If provided, the predicate is invoked with both the source path and the
|
|
268
|
+
destination path as arguments. Only pairs selected by the predicate are returned.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
An iterator over all matching deployed artifacts.
|
|
272
|
+
"""
|
|
273
|
+
for src in self._src_to_dest.keys():
|
|
274
|
+
for deployed_src, deployed_dest in self._mappings_for_source(
|
|
275
|
+
src,
|
|
276
|
+
absolute=absolute,
|
|
277
|
+
expand_directories=expand_directories,
|
|
278
|
+
predicate=predicate,
|
|
279
|
+
):
|
|
280
|
+
yield deployed_src, deployed_dest
|
|
281
|
+
|
|
282
|
+
def to_deploy_paths(self, src: Path) -> List[Path]:
|
|
283
|
+
"""
|
|
284
|
+
Converts a source path to its corresponding deploy root path. If the input path is relative to the project root,
|
|
285
|
+
a path relative to the deploy root is returned. If the input path is absolute, an absolute path is returned.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The deploy root paths for the given source path, or an empty list if no such path exists.
|
|
289
|
+
"""
|
|
290
|
+
is_absolute = src.is_absolute()
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
absolute_src = self._absolute_src(src)
|
|
294
|
+
if not absolute_src.exists():
|
|
295
|
+
return []
|
|
296
|
+
canonical_src = self._canonical_src(absolute_src)
|
|
297
|
+
except ArtifactError:
|
|
298
|
+
# No mapping is possible for this src path
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
output_destinations: List[Path] = []
|
|
302
|
+
|
|
303
|
+
canonical_dests = self._src_to_dest.get(canonical_src)
|
|
304
|
+
if canonical_dests is not None:
|
|
305
|
+
for d in canonical_dests:
|
|
306
|
+
output_destinations.append(self._to_output_dest(d, is_absolute))
|
|
307
|
+
|
|
308
|
+
canonical_parent = canonical_src.parent
|
|
309
|
+
canonical_parent_dests = self.to_deploy_paths(canonical_parent)
|
|
310
|
+
if canonical_parent_dests:
|
|
311
|
+
canonical_child = canonical_src.relative_to(canonical_parent)
|
|
312
|
+
for d in canonical_parent_dests:
|
|
313
|
+
output_destinations.append(
|
|
314
|
+
self._to_output_dest(d / canonical_child, is_absolute)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return output_destinations
|
|
318
|
+
|
|
319
|
+
def _absolute_src(self, src: Path) -> Path:
|
|
320
|
+
if src.is_absolute():
|
|
321
|
+
resolved_src = resolve_without_follow(src)
|
|
322
|
+
else:
|
|
323
|
+
resolved_src = resolve_without_follow(self._project_root / src)
|
|
324
|
+
if self._project_root not in resolved_src.parents:
|
|
325
|
+
raise ArtifactError(
|
|
326
|
+
f"Source is not in the project root: {src}, root={self._project_root}"
|
|
327
|
+
)
|
|
328
|
+
return resolved_src
|
|
329
|
+
|
|
330
|
+
def _absolute_dest(self, dest: Path, src_path: Optional[Path] = None) -> Path:
|
|
331
|
+
if dest.is_absolute():
|
|
332
|
+
resolved_dest = resolve_without_follow(dest)
|
|
333
|
+
else:
|
|
334
|
+
resolved_dest = resolve_without_follow(self._deploy_root / dest)
|
|
335
|
+
if (
|
|
336
|
+
self._deploy_root != resolved_dest
|
|
337
|
+
and self._deploy_root not in resolved_dest.parents
|
|
338
|
+
):
|
|
339
|
+
raise NotInDeployRootError(
|
|
340
|
+
dest_path=dest, deploy_root=self._deploy_root, src_path=src_path
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return resolved_dest
|
|
344
|
+
|
|
345
|
+
def _canonical_src(self, src: Path) -> Path:
|
|
346
|
+
"""
|
|
347
|
+
Returns the canonical version of a source path, relative to the project root.
|
|
348
|
+
"""
|
|
349
|
+
absolute_src = self._absolute_src(src)
|
|
350
|
+
return absolute_src.relative_to(self._project_root)
|
|
351
|
+
|
|
352
|
+
def _canonical_dest(self, dest: Path) -> Path:
|
|
353
|
+
"""
|
|
354
|
+
Returns the canonical version of a destination path, relative to the deploy root.
|
|
355
|
+
"""
|
|
356
|
+
absolute_dest = self._absolute_dest(dest)
|
|
357
|
+
return absolute_dest.relative_to(self._deploy_root)
|
|
358
|
+
|
|
359
|
+
def _to_output_dest(self, dest: Path, absolute: bool) -> Path:
|
|
360
|
+
return self._absolute_dest(dest) if absolute else self._canonical_dest(dest)
|
|
361
|
+
|
|
362
|
+
def _to_output_src(self, src: Path, absolute: bool) -> Path:
|
|
363
|
+
return self._absolute_src(src) if absolute else self._canonical_src(src)
|
|
364
|
+
|
|
365
|
+
def _update_dest_is_dir(self, canonical_dest: Path, is_dir: bool) -> None:
|
|
366
|
+
current_is_dir = self._dest_is_dir.get(canonical_dest, None)
|
|
367
|
+
if current_is_dir is not None and is_dir != current_is_dir:
|
|
368
|
+
raise ArtifactError(
|
|
369
|
+
"Conflicting type for destination path: {canonical_dest}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
parent = canonical_dest.parent
|
|
373
|
+
if parent != canonical_dest:
|
|
374
|
+
self._update_dest_is_dir(parent, True)
|
|
375
|
+
|
|
376
|
+
self._dest_is_dir[canonical_dest] = is_dir
|
|
107
377
|
|
|
108
378
|
|
|
109
379
|
def specifies_directory(s: str) -> bool:
|
|
@@ -132,7 +402,9 @@ def delete(path: Path) -> None:
|
|
|
132
402
|
spath.rmdir(recursive=True) # remove dir and all contains
|
|
133
403
|
|
|
134
404
|
|
|
135
|
-
def symlink_or_copy(
|
|
405
|
+
def symlink_or_copy(
|
|
406
|
+
src: Path, dst: Path, deploy_root: Path, makedirs=True, overwrite=True
|
|
407
|
+
) -> None:
|
|
136
408
|
"""
|
|
137
409
|
Tries to create a symlink to src at dst; failing that (i.e. in Windows
|
|
138
410
|
without Administrator / Developer Mode) copies the file from src to dst instead.
|
|
@@ -143,6 +415,14 @@ def symlink_or_copy(src: Path, dst: Path, makedirs=True, overwrite=True) -> None
|
|
|
143
415
|
sdst = SecurePath(dst)
|
|
144
416
|
if makedirs:
|
|
145
417
|
sdst.parent.mkdir(parents=True, exist_ok=True)
|
|
418
|
+
|
|
419
|
+
# Verify that the mapping isn't accidentally trying to create a file in the project source through symlinks.
|
|
420
|
+
# We need to ensure we're resolving symlinks for this check to be effective.
|
|
421
|
+
resolved_dst = dst.resolve()
|
|
422
|
+
resolved_deploy_root = deploy_root.resolve()
|
|
423
|
+
if resolved_deploy_root not in resolved_dst.parents:
|
|
424
|
+
raise NotInDeployRootError(dest_path=dst, deploy_root=deploy_root, src_path=src)
|
|
425
|
+
|
|
146
426
|
if overwrite:
|
|
147
427
|
delete(dst)
|
|
148
428
|
try:
|
|
@@ -151,7 +431,7 @@ def symlink_or_copy(src: Path, dst: Path, makedirs=True, overwrite=True) -> None
|
|
|
151
431
|
ssrc.copy(dst)
|
|
152
432
|
|
|
153
433
|
|
|
154
|
-
def translate_artifact(item: Union[
|
|
434
|
+
def translate_artifact(item: Union[PathMapping, str]) -> ArtifactMapping:
|
|
155
435
|
"""
|
|
156
436
|
Builds an artifact mapping from a project definition value.
|
|
157
437
|
Validation is done later when we actually resolve files / folders.
|
|
@@ -167,28 +447,6 @@ def translate_artifact(item: Union[dict, str]) -> ArtifactMapping:
|
|
|
167
447
|
raise ArtifactError("Item is not a valid artifact!")
|
|
168
448
|
|
|
169
449
|
|
|
170
|
-
def get_source_paths(artifact: ArtifactMapping, project_root: Path) -> List[Path]:
|
|
171
|
-
"""
|
|
172
|
-
Expands globs, ensuring at least one file exists that matches artifact.src.
|
|
173
|
-
Returns a list of paths that resolve to actual files in the project root dir structure.
|
|
174
|
-
If a glob does not specify a directory (i.e. does not end with a path separator)
|
|
175
|
-
|
|
176
|
-
"""
|
|
177
|
-
source_paths: List[Path]
|
|
178
|
-
|
|
179
|
-
if is_glob(artifact.src):
|
|
180
|
-
source_paths = list(project_root.glob(artifact.src))
|
|
181
|
-
if not source_paths:
|
|
182
|
-
raise GlobMatchedNothingError(artifact.src)
|
|
183
|
-
else:
|
|
184
|
-
source_path = Path(project_root, artifact.src)
|
|
185
|
-
source_paths = [source_path]
|
|
186
|
-
if not source_path.exists():
|
|
187
|
-
raise SourceNotFoundError(source_path)
|
|
188
|
-
|
|
189
|
-
return source_paths
|
|
190
|
-
|
|
191
|
-
|
|
192
450
|
def resolve_without_follow(path: Path) -> Path:
|
|
193
451
|
"""
|
|
194
452
|
Resolves a Path to an absolute version of itself, without following
|
|
@@ -201,7 +459,7 @@ def build_bundle(
|
|
|
201
459
|
project_root: Path,
|
|
202
460
|
deploy_root: Path,
|
|
203
461
|
artifacts: List[ArtifactMapping],
|
|
204
|
-
) ->
|
|
462
|
+
) -> BundleMap:
|
|
205
463
|
"""
|
|
206
464
|
Prepares a local folder (deploy_root) with configured app artifacts.
|
|
207
465
|
This folder can then be uploaded to a stage.
|
|
@@ -223,34 +481,16 @@ def build_bundle(
|
|
|
223
481
|
if resolved_root.exists():
|
|
224
482
|
delete(resolved_root)
|
|
225
483
|
|
|
226
|
-
|
|
484
|
+
bundle_map = BundleMap(project_root=project_root, deploy_root=deploy_root)
|
|
227
485
|
for artifact in artifacts:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# copy all files as children of the given destination path
|
|
237
|
-
for source_path in source_paths:
|
|
238
|
-
dest_child_path = dest_path / source_path.name
|
|
239
|
-
symlink_or_copy(source_path, dest_child_path)
|
|
240
|
-
mapped_files[source_path.resolve()] = dest_child_path
|
|
241
|
-
else:
|
|
242
|
-
# ensure we are copying into the deploy root, not replacing it!
|
|
243
|
-
if resolved_root not in dest_path.parents:
|
|
244
|
-
raise NotInDeployRootError(artifact.src, dest_path, resolved_root)
|
|
245
|
-
|
|
246
|
-
if len(source_paths) == 1:
|
|
247
|
-
# copy a single file as the given destination path
|
|
248
|
-
symlink_or_copy(source_paths[0], dest_path)
|
|
249
|
-
mapped_files[source_paths[0].resolve()] = dest_path
|
|
250
|
-
else:
|
|
251
|
-
# refuse to map multiple source files to one destination (undefined behaviour)
|
|
252
|
-
raise TooManyFilesError(dest_path)
|
|
253
|
-
return mapped_files
|
|
486
|
+
bundle_map.add(artifact)
|
|
487
|
+
|
|
488
|
+
for (absolute_src, absolute_dest) in bundle_map.all_mappings(
|
|
489
|
+
absolute=True, expand_directories=False
|
|
490
|
+
):
|
|
491
|
+
symlink_or_copy(absolute_src, absolute_dest, deploy_root=deploy_root)
|
|
492
|
+
|
|
493
|
+
return bundle_map
|
|
254
494
|
|
|
255
495
|
|
|
256
496
|
def find_manifest_file(deploy_root: Path) -> Path:
|
|
@@ -268,6 +508,43 @@ def find_manifest_file(deploy_root: Path) -> Path:
|
|
|
268
508
|
)
|
|
269
509
|
|
|
270
510
|
|
|
511
|
+
def find_and_read_manifest_file(deploy_root: Path) -> Dict[str, Any]:
|
|
512
|
+
"""
|
|
513
|
+
Finds the manifest file in the deploy root of the project, and reads the contents and returns them
|
|
514
|
+
as a dictionary.
|
|
515
|
+
"""
|
|
516
|
+
manifest_file = find_manifest_file(deploy_root=deploy_root)
|
|
517
|
+
with SecurePath(manifest_file).open(
|
|
518
|
+
"r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
|
|
519
|
+
) as file:
|
|
520
|
+
manifest_content = safe_load(file.read())
|
|
521
|
+
return manifest_content
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def find_setup_script_file(deploy_root: Path) -> Path:
|
|
525
|
+
"""
|
|
526
|
+
Find the setup script file, if available, in the deploy_root of the Snowflake Native App project.
|
|
527
|
+
"""
|
|
528
|
+
artifacts = "artifacts"
|
|
529
|
+
setup_script = "setup_script"
|
|
530
|
+
|
|
531
|
+
manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
|
|
532
|
+
|
|
533
|
+
if (artifacts in manifest_content) and (
|
|
534
|
+
setup_script in manifest_content[artifacts]
|
|
535
|
+
):
|
|
536
|
+
setup_script_rel_path = manifest_content[artifacts][setup_script]
|
|
537
|
+
file_name = Path(deploy_root / setup_script_rel_path)
|
|
538
|
+
if file_name.is_file():
|
|
539
|
+
return file_name
|
|
540
|
+
else:
|
|
541
|
+
raise ClickException(f"Could not find setup script file at {file_name}.")
|
|
542
|
+
else:
|
|
543
|
+
raise ClickException(
|
|
544
|
+
"Manifest.yml file must contain an artifacts section to specify the location of the setup script."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
271
548
|
def find_version_info_in_manifest_file(
|
|
272
549
|
deploy_root: Path,
|
|
273
550
|
) -> Tuple[Optional[str], Optional[str]]:
|
|
@@ -278,11 +555,7 @@ def find_version_info_in_manifest_file(
|
|
|
278
555
|
name_field = "name"
|
|
279
556
|
patch_field = "patch"
|
|
280
557
|
|
|
281
|
-
|
|
282
|
-
with SecurePath(manifest_file).open(
|
|
283
|
-
"r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
|
|
284
|
-
) as file:
|
|
285
|
-
manifest_content = safe_load(file.read())
|
|
558
|
+
manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
|
|
286
559
|
|
|
287
560
|
version_name: Optional[str] = None
|
|
288
561
|
patch_name: Optional[str] = None
|
|
@@ -294,31 +567,3 @@ def find_version_info_in_manifest_file(
|
|
|
294
567
|
patch_name = version_info[patch_field]
|
|
295
568
|
|
|
296
569
|
return version_name, patch_name
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def source_path_to_deploy_path(
|
|
300
|
-
source_path: Path, mapped_files: ArtifactDeploymentMap
|
|
301
|
-
) -> Path:
|
|
302
|
-
"""Returns the absolute path where the specified source path was copied to during bundle."""
|
|
303
|
-
|
|
304
|
-
source_path = source_path.resolve()
|
|
305
|
-
|
|
306
|
-
if source_path in mapped_files:
|
|
307
|
-
return mapped_files[source_path]
|
|
308
|
-
|
|
309
|
-
# Find the first parent directory that exists in mapped_files
|
|
310
|
-
common_root = source_path
|
|
311
|
-
while common_root:
|
|
312
|
-
if common_root in mapped_files:
|
|
313
|
-
break
|
|
314
|
-
elif common_root.parent != common_root:
|
|
315
|
-
common_root = common_root.parent
|
|
316
|
-
else:
|
|
317
|
-
raise ClickException(f"Could not find the deploy path of {source_path}")
|
|
318
|
-
|
|
319
|
-
# Construct the target deploy path
|
|
320
|
-
path_to_symlink = mapped_files[common_root]
|
|
321
|
-
relative_path_to_target = Path(source_path).relative_to(common_root)
|
|
322
|
-
result = Path(path_to_symlink, relative_path_to_target)
|
|
323
|
-
|
|
324
|
-
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from click import ClickException
|
|
8
|
+
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
|
|
9
|
+
from snowflake.cli.api.project.schemas.native_app.path_mapping import (
|
|
10
|
+
PathMapping,
|
|
11
|
+
ProcessorMapping,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UnsupportedArtifactProcessorError(ClickException):
|
|
16
|
+
"""Exception thrown when a user has passed in an unsupported artifact processor."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, processor_name: str):
|
|
19
|
+
super().__init__(
|
|
20
|
+
f"Unsupported value {processor_name} detected for an artifact processor. Please refer to documentation for a list of supported types."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArtifactProcessor(ABC):
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
project_definition: NativeApp,
|
|
29
|
+
project_root: Path,
|
|
30
|
+
deploy_root: Path,
|
|
31
|
+
generated_root: Path,
|
|
32
|
+
**kwargs,
|
|
33
|
+
) -> None:
|
|
34
|
+
assert project_root.is_absolute()
|
|
35
|
+
assert deploy_root.is_absolute()
|
|
36
|
+
assert generated_root.is_absolute()
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def process(
|
|
40
|
+
self,
|
|
41
|
+
artifact_to_process: PathMapping,
|
|
42
|
+
processor_mapping: Optional[ProcessorMapping],
|
|
43
|
+
**kwargs,
|
|
44
|
+
) -> None:
|
|
45
|
+
pass
|