snowflake-cli 3.3.0__py3-none-any.whl → 3.5.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/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +220 -197
- snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +4 -30
- snowflake/cli/_app/printing.py +2 -2
- snowflake/cli/_plugins/connection/commands.py +2 -4
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +3 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/commands.py +21 -19
- snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
- snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
- snowflake/cli/_plugins/notebook/commands.py +54 -2
- snowflake/cli/_plugins/notebook/exceptions.py +1 -1
- snowflake/cli/_plugins/notebook/manager.py +3 -3
- snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
- snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
- snowflake/cli/_plugins/notebook/types.py +3 -0
- 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 +157 -0
- snowflake/cli/_plugins/project/feature_flags.py +22 -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 +49 -30
- snowflake/cli/_plugins/snowpark/common.py +47 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
- snowflake/cli/_plugins/snowpark/zipper.py +33 -1
- snowflake/cli/_plugins/spcs/compute_pool/commands.py +53 -5
- 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 +51 -1
- 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/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +26 -13
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +18 -24
- snowflake/cli/_plugins/streamlit/manager.py +37 -27
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
- snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
- snowflake/cli/_plugins/workspace/commands.py +3 -3
- snowflake/cli/_plugins/workspace/manager.py +1 -1
- snowflake/cli/api/artifacts/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/upload.py +51 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +14 -1
- snowflake/cli/api/commands/flags.py +34 -13
- snowflake/cli/api/commands/snow_typer.py +12 -0
- snowflake/cli/api/commands/utils.py +30 -2
- snowflake/cli/api/config.py +15 -10
- snowflake/cli/api/constants.py +1 -0
- snowflake/cli/api/entities/common.py +14 -32
- snowflake/cli/api/entities/resolver.py +160 -0
- snowflake/cli/api/entities/utils.py +56 -15
- snowflake/cli/api/errno.py +3 -0
- snowflake/cli/api/exceptions.py +8 -1
- snowflake/cli/api/feature_flags.py +1 -1
- snowflake/cli/api/plugins/plugin_config.py +43 -4
- snowflake/cli/api/project/definition_conversion.py +3 -2
- snowflake/cli/api/project/definition_helper.py +31 -0
- snowflake/cli/api/project/project_paths.py +28 -0
- snowflake/cli/api/project/schemas/entities/common.py +130 -1
- snowflake/cli/api/project/schemas/entities/entities.py +30 -0
- snowflake/cli/api/project/schemas/project_definition.py +27 -0
- snowflake/cli/api/project/schemas/updatable_model.py +2 -2
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
- snowflake/cli/api/secure_path.py +6 -0
- snowflake/cli/api/sql_execution.py +5 -1
- snowflake/cli/api/stage_path.py +7 -2
- snowflake/cli/api/utils/graph.py +3 -0
- snowflake/cli/api/utils/path_utils.py +24 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +12 -13
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +109 -85
- snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
- snowflake/cli/api/__init__.py +0 -48
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
- /snowflake/cli/{_app/api_impl/plugin → api/artifacts}/__init__.py +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from snowflake.cli.api.artifacts.common import (
|
|
9
|
+
ArtifactError,
|
|
10
|
+
NotInDeployRootError,
|
|
11
|
+
SourceNotFoundError,
|
|
12
|
+
TooManyFilesError,
|
|
13
|
+
)
|
|
14
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
15
|
+
from snowflake.cli.api.utils.path_utils import resolve_without_follow
|
|
16
|
+
|
|
17
|
+
ArtifactPredicate = Callable[[Path, Path], bool]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _specifies_directory(s: str) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Does the path (as seen from the project definition) refer to
|
|
23
|
+
a directory? For destination paths, we enforce the usage of a
|
|
24
|
+
trailing forward slash (/). Note that we use the forward slash
|
|
25
|
+
even on Windows so that snowflake.yml can be shared between OSes.
|
|
26
|
+
|
|
27
|
+
This means that to put a file in the root of the stage, we need
|
|
28
|
+
to specify "./" as its destination, or omit it (but only if the
|
|
29
|
+
file already lives in the project root).
|
|
30
|
+
"""
|
|
31
|
+
return s.endswith("/")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BundleMap:
|
|
35
|
+
"""
|
|
36
|
+
Computes the mapping between project directory artifacts (aka source artifacts) to their deploy root location
|
|
37
|
+
(aka destination artifact).
|
|
38
|
+
|
|
39
|
+
:param project_root: The root directory of the project and base for all relative paths. Must be an absolute path.
|
|
40
|
+
:param deploy_root: The directory where artifacts should be copied to. Must be an absolute path.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, *, project_root: Path, deploy_root: Path):
|
|
44
|
+
# If a relative path ends up here, it's a bug in the app and can lead to other
|
|
45
|
+
# subtle bugs as paths would be resolved relative to the current working directory.
|
|
46
|
+
assert (
|
|
47
|
+
project_root.is_absolute()
|
|
48
|
+
), f"Project root {project_root} must be an absolute path."
|
|
49
|
+
assert (
|
|
50
|
+
deploy_root.is_absolute()
|
|
51
|
+
), f"Deploy root {deploy_root} must be an absolute path."
|
|
52
|
+
|
|
53
|
+
self._project_root: Path = resolve_without_follow(project_root)
|
|
54
|
+
self._deploy_root: Path = resolve_without_follow(deploy_root)
|
|
55
|
+
self._artifact_map = _ArtifactPathMap(project_root=self._project_root)
|
|
56
|
+
|
|
57
|
+
def is_empty(self) -> bool:
|
|
58
|
+
return self._artifact_map.is_empty()
|
|
59
|
+
|
|
60
|
+
def deploy_root(self) -> Path:
|
|
61
|
+
return self._deploy_root
|
|
62
|
+
|
|
63
|
+
def project_root(self) -> Path:
|
|
64
|
+
return self._project_root
|
|
65
|
+
|
|
66
|
+
def _add(self, src: Path, dest: Path, map_as_child: bool) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Adds the specified artifact mapping rule to this map.
|
|
69
|
+
|
|
70
|
+
Arguments:
|
|
71
|
+
src {Path} -- the source path
|
|
72
|
+
dest {Path} -- the destination path
|
|
73
|
+
map_as_child {bool} -- when True, the source will be added as a child of the specified destination.
|
|
74
|
+
"""
|
|
75
|
+
absolute_src = self._absolute_src(src)
|
|
76
|
+
absolute_dest = self._absolute_dest(dest, src_path=src)
|
|
77
|
+
dest_is_dir = absolute_src.is_dir() or map_as_child
|
|
78
|
+
|
|
79
|
+
# Check for the special case of './' as a target ('.' is not allowed)
|
|
80
|
+
if absolute_dest == self._deploy_root and not map_as_child:
|
|
81
|
+
raise NotInDeployRootError(
|
|
82
|
+
dest_path=dest, deploy_root=self._deploy_root, src_path=src
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if self._deploy_root in absolute_src.parents:
|
|
86
|
+
# ignore this item since it's in the deploy root. This can happen if the bundle map is created
|
|
87
|
+
# after the bundle step and a project is using rules that are not sufficiently constrained.
|
|
88
|
+
# Since the bundle step starts with deleting the deploy root, we wouldn't normally encounter this situation.
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
canonical_src = self._canonical_src(src)
|
|
92
|
+
canonical_dest = self._canonical_dest(dest)
|
|
93
|
+
|
|
94
|
+
if map_as_child:
|
|
95
|
+
# Make sure the destination is a child of the original, since this was requested
|
|
96
|
+
canonical_dest = canonical_dest / canonical_src.name
|
|
97
|
+
dest_is_dir = absolute_src.is_dir()
|
|
98
|
+
|
|
99
|
+
self._artifact_map.put(
|
|
100
|
+
src=canonical_src, dest=canonical_dest, dest_is_dir=dest_is_dir
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _add_mapping(self, src: str, dest: Optional[str] = None):
|
|
104
|
+
"""
|
|
105
|
+
Adds the specified artifact rule to this instance. The source should be relative to the project directory. It
|
|
106
|
+
is interpreted as a file, directory or glob pattern. If the destination path is not specified, each source match
|
|
107
|
+
is mapped to an identical path in the deploy root.
|
|
108
|
+
"""
|
|
109
|
+
match_found = False
|
|
110
|
+
|
|
111
|
+
src_path = Path(src)
|
|
112
|
+
if src_path.is_absolute():
|
|
113
|
+
raise ArtifactError("Source path must be a relative path")
|
|
114
|
+
|
|
115
|
+
for resolved_src in self._project_root.glob(src):
|
|
116
|
+
match_found = True
|
|
117
|
+
|
|
118
|
+
if dest:
|
|
119
|
+
dest_stem = dest.rstrip("/")
|
|
120
|
+
if not dest_stem:
|
|
121
|
+
# handle '/' as the destination as a special case. This is because specifying only '/' as a
|
|
122
|
+
# a destination looks like '.' once all forwards slashes are stripped. If we don't handle it
|
|
123
|
+
# specially here, `dest: /` would incorrectly be allowed.
|
|
124
|
+
raise NotInDeployRootError(
|
|
125
|
+
dest_path=dest,
|
|
126
|
+
deploy_root=self._deploy_root,
|
|
127
|
+
src_path=resolved_src,
|
|
128
|
+
)
|
|
129
|
+
dest_path = Path(dest.rstrip("/"))
|
|
130
|
+
if dest_path.is_absolute():
|
|
131
|
+
raise ArtifactError("Destination path must be a relative path")
|
|
132
|
+
self._add(resolved_src, dest_path, _specifies_directory(dest))
|
|
133
|
+
else:
|
|
134
|
+
self._add(
|
|
135
|
+
resolved_src,
|
|
136
|
+
resolved_src.relative_to(self._project_root),
|
|
137
|
+
False,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if not match_found:
|
|
141
|
+
raise SourceNotFoundError(src)
|
|
142
|
+
|
|
143
|
+
def add(self, mapping: PathMapping) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Adds an artifact mapping rule to this instance.
|
|
146
|
+
"""
|
|
147
|
+
self._add_mapping(mapping.src, mapping.dest)
|
|
148
|
+
|
|
149
|
+
def _expand_artifact_mapping(
|
|
150
|
+
self,
|
|
151
|
+
src: Path,
|
|
152
|
+
dest: Path,
|
|
153
|
+
absolute: bool = False,
|
|
154
|
+
expand_directories: bool = False,
|
|
155
|
+
predicate: ArtifactPredicate = lambda src, dest: True,
|
|
156
|
+
) -> Iterator[Tuple[Path, Path]]:
|
|
157
|
+
"""
|
|
158
|
+
Expands the specified source-destination mapping according to the provided options.
|
|
159
|
+
The original mapping is yielded, followed by any expanded mappings derived from
|
|
160
|
+
it.
|
|
161
|
+
|
|
162
|
+
Arguments:
|
|
163
|
+
src {Path} -- the source path
|
|
164
|
+
dest {Path} -- the destination path
|
|
165
|
+
absolute {bool} -- when True, all mappings will be yielded as absolute paths
|
|
166
|
+
expand_directories {bool} -- when True, child mappings are yielded if the source path is a directory.
|
|
167
|
+
predicate {ArtifactPredicate} -- when specified, only mappings satisfying this predicate will be yielded.
|
|
168
|
+
"""
|
|
169
|
+
canonical_src = self._canonical_src(src)
|
|
170
|
+
canonical_dest = self._canonical_dest(dest)
|
|
171
|
+
|
|
172
|
+
absolute_src = self._absolute_src(canonical_src)
|
|
173
|
+
absolute_dest = self._absolute_dest(canonical_dest)
|
|
174
|
+
src_for_output = self._to_output_src(absolute_src, absolute)
|
|
175
|
+
dest_for_output = self._to_output_dest(absolute_dest, absolute)
|
|
176
|
+
|
|
177
|
+
if predicate(src_for_output, dest_for_output):
|
|
178
|
+
yield src_for_output, dest_for_output
|
|
179
|
+
|
|
180
|
+
if absolute_src.is_dir() and expand_directories:
|
|
181
|
+
# both src and dest are directories, and expanding directories was requested. Traverse src, and map each
|
|
182
|
+
# file to the dest directory
|
|
183
|
+
for root, subdirs, files in os.walk(absolute_src, followlinks=True):
|
|
184
|
+
relative_root = Path(root).relative_to(absolute_src)
|
|
185
|
+
for name in itertools.chain(subdirs, files):
|
|
186
|
+
src_file_for_output = src_for_output / relative_root / name
|
|
187
|
+
dest_file_for_output = dest_for_output / relative_root / name
|
|
188
|
+
if predicate(src_file_for_output, dest_file_for_output):
|
|
189
|
+
yield src_file_for_output, dest_file_for_output
|
|
190
|
+
|
|
191
|
+
def all_mappings(
|
|
192
|
+
self,
|
|
193
|
+
absolute: bool = False,
|
|
194
|
+
expand_directories: bool = False,
|
|
195
|
+
predicate: ArtifactPredicate = lambda src, dest: True,
|
|
196
|
+
) -> Iterator[Tuple[Path, Path]]:
|
|
197
|
+
"""
|
|
198
|
+
Yields a (src, dest) pair for each deployed artifact in the project. Each pair corresponds to a single file
|
|
199
|
+
in the project. Source directories are resolved as needed to resolve their contents.
|
|
200
|
+
|
|
201
|
+
Arguments:
|
|
202
|
+
self: this instance
|
|
203
|
+
absolute (bool): Specifies whether the yielded paths should be joined with the project or deploy roots,
|
|
204
|
+
as appropriate.
|
|
205
|
+
expand_directories (bool): Specifies whether directory to directory mappings should be expanded to
|
|
206
|
+
resolve their contained files.
|
|
207
|
+
predicate (PathPredicate): If provided, the predicate is invoked with both the source path and the
|
|
208
|
+
destination path as arguments. Only pairs selected by the predicate are returned.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
An iterator over all matching deployed artifacts.
|
|
212
|
+
"""
|
|
213
|
+
for src, dest in self._artifact_map:
|
|
214
|
+
for deployed_src, deployed_dest in self._expand_artifact_mapping(
|
|
215
|
+
src,
|
|
216
|
+
dest,
|
|
217
|
+
absolute=absolute,
|
|
218
|
+
expand_directories=expand_directories,
|
|
219
|
+
predicate=predicate,
|
|
220
|
+
):
|
|
221
|
+
yield deployed_src, deployed_dest
|
|
222
|
+
|
|
223
|
+
def to_deploy_paths(self, src: Path) -> List[Path]:
|
|
224
|
+
"""
|
|
225
|
+
Converts a source path to its corresponding deploy root path. If the input path is relative to the project root,
|
|
226
|
+
paths relative to the deploy root are returned. If the input path is absolute, absolute paths are returned.
|
|
227
|
+
|
|
228
|
+
Note that the provided source path must be part of a mapping. If the source path is not part of any mapping,
|
|
229
|
+
an empty list is returned. For example, if `app/*` is specified as the source of a mapping,
|
|
230
|
+
`to_deploy_paths(Path("app"))` will not yield any result.
|
|
231
|
+
|
|
232
|
+
Arguments:
|
|
233
|
+
src {Path} -- the source path within the project root, in canonical or absolute form.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The deploy root paths for the given source path, or an empty list if no such path exists.
|
|
237
|
+
"""
|
|
238
|
+
is_absolute = src.is_absolute()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
absolute_src = self._absolute_src(src)
|
|
242
|
+
if not absolute_src.exists():
|
|
243
|
+
return []
|
|
244
|
+
canonical_src = self._canonical_src(absolute_src)
|
|
245
|
+
except ArtifactError:
|
|
246
|
+
# No mapping is possible for this src path
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
output_destinations: List[Path] = []
|
|
250
|
+
|
|
251
|
+
# 1. Check for exact rule matches for this path
|
|
252
|
+
canonical_dests = self._artifact_map.get_destinations(canonical_src)
|
|
253
|
+
if canonical_dests:
|
|
254
|
+
for d in canonical_dests:
|
|
255
|
+
output_destinations.append(self._to_output_dest(d, is_absolute))
|
|
256
|
+
|
|
257
|
+
# 2. Check for any matches to parent directories for this path that would
|
|
258
|
+
# cause this path to be part of the recursive copy
|
|
259
|
+
canonical_parent = canonical_src.parent
|
|
260
|
+
canonical_parent_dests = self.to_deploy_paths(canonical_parent)
|
|
261
|
+
if canonical_parent_dests:
|
|
262
|
+
canonical_child = canonical_src.relative_to(canonical_parent)
|
|
263
|
+
for d in canonical_parent_dests:
|
|
264
|
+
output_destinations.append(
|
|
265
|
+
self._to_output_dest(d / canonical_child, is_absolute)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return output_destinations
|
|
269
|
+
|
|
270
|
+
def all_sources(self, absolute: bool = False) -> Iterator[Path]:
|
|
271
|
+
"""
|
|
272
|
+
Yields each registered artifact source in the project.
|
|
273
|
+
|
|
274
|
+
Arguments:
|
|
275
|
+
self: this instance
|
|
276
|
+
absolute (bool): Specifies whether the yielded paths should be joined with the absolute project root.
|
|
277
|
+
Returns:
|
|
278
|
+
An iterator over all artifact mapping source paths.
|
|
279
|
+
"""
|
|
280
|
+
for src in self._artifact_map.all_sources():
|
|
281
|
+
yield self._to_output_src(src, absolute)
|
|
282
|
+
|
|
283
|
+
def to_project_path(self, dest: Path) -> Optional[Path]:
|
|
284
|
+
"""
|
|
285
|
+
Converts a deploy root path to its corresponding project source path. If the input path is relative to the
|
|
286
|
+
deploy root, a path relative to the project root is returned. If the input path is absolute, an absolute path is
|
|
287
|
+
returned.
|
|
288
|
+
|
|
289
|
+
Arguments:
|
|
290
|
+
dest {Path} -- the destination path within the deploy root, in canonical or absolute form.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
The project root path for the given deploy root path, or None if no such path exists.
|
|
294
|
+
"""
|
|
295
|
+
is_absolute = dest.is_absolute()
|
|
296
|
+
try:
|
|
297
|
+
canonical_dest = self._canonical_dest(dest)
|
|
298
|
+
except NotInDeployRootError:
|
|
299
|
+
# No mapping possible for the dest path
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
# 1. Look for an exact rule matching this path. If we find any, then
|
|
303
|
+
# stop searching. This is because each destination path can only originate
|
|
304
|
+
# from a single source (however, one source can be copied to multiple destinations).
|
|
305
|
+
canonical_src = self._artifact_map.get_source(canonical_dest)
|
|
306
|
+
if canonical_src is not None:
|
|
307
|
+
return self._to_output_src(canonical_src, is_absolute)
|
|
308
|
+
|
|
309
|
+
# 2. No exact match was found, look for a match for parent directories of this
|
|
310
|
+
# path, recursively. Stop when a match is found
|
|
311
|
+
canonical_parent = canonical_dest.parent
|
|
312
|
+
if canonical_parent == canonical_dest:
|
|
313
|
+
return None
|
|
314
|
+
canonical_parent_src = self.to_project_path(canonical_parent)
|
|
315
|
+
if canonical_parent_src is not None:
|
|
316
|
+
canonical_child = canonical_dest.relative_to(canonical_parent)
|
|
317
|
+
canonical_child_candidate = canonical_parent_src / canonical_child
|
|
318
|
+
if self._absolute_src(canonical_child_candidate).exists():
|
|
319
|
+
return self._to_output_src(canonical_child_candidate, is_absolute)
|
|
320
|
+
|
|
321
|
+
# No mapping for this destination path
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def _absolute_src(self, src: Path) -> Path:
|
|
325
|
+
if src.is_absolute():
|
|
326
|
+
resolved_src = resolve_without_follow(src)
|
|
327
|
+
else:
|
|
328
|
+
resolved_src = resolve_without_follow(self._project_root / src)
|
|
329
|
+
if self._project_root not in resolved_src.parents:
|
|
330
|
+
raise ArtifactError(
|
|
331
|
+
f"Source is not in the project root: {src}, root={self._project_root}"
|
|
332
|
+
)
|
|
333
|
+
return resolved_src
|
|
334
|
+
|
|
335
|
+
def _absolute_dest(self, dest: Path, src_path: Optional[Path] = None) -> Path:
|
|
336
|
+
if dest.is_absolute():
|
|
337
|
+
resolved_dest = resolve_without_follow(dest)
|
|
338
|
+
else:
|
|
339
|
+
resolved_dest = resolve_without_follow(self._deploy_root / dest)
|
|
340
|
+
if (
|
|
341
|
+
self._deploy_root != resolved_dest
|
|
342
|
+
and self._deploy_root not in resolved_dest.parents
|
|
343
|
+
):
|
|
344
|
+
raise NotInDeployRootError(
|
|
345
|
+
dest_path=dest, deploy_root=self._deploy_root, src_path=src_path
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return resolved_dest
|
|
349
|
+
|
|
350
|
+
def _canonical_src(self, src: Path) -> Path:
|
|
351
|
+
"""
|
|
352
|
+
Returns the canonical version of a source path, relative to the project root.
|
|
353
|
+
"""
|
|
354
|
+
absolute_src = self._absolute_src(src)
|
|
355
|
+
return absolute_src.relative_to(self._project_root)
|
|
356
|
+
|
|
357
|
+
def _canonical_dest(self, dest: Path) -> Path:
|
|
358
|
+
"""
|
|
359
|
+
Returns the canonical version of a destination path, relative to the deploy root.
|
|
360
|
+
"""
|
|
361
|
+
absolute_dest = self._absolute_dest(dest)
|
|
362
|
+
return absolute_dest.relative_to(self._deploy_root)
|
|
363
|
+
|
|
364
|
+
def _to_output_dest(self, dest: Path, absolute: bool) -> Path:
|
|
365
|
+
return self._absolute_dest(dest) if absolute else self._canonical_dest(dest)
|
|
366
|
+
|
|
367
|
+
def _to_output_src(self, src: Path, absolute: bool) -> Path:
|
|
368
|
+
return self._absolute_src(src) if absolute else self._canonical_src(src)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class _ArtifactPathMap:
|
|
372
|
+
"""
|
|
373
|
+
A specialized version of an ordered multimap used to keep track of artifact
|
|
374
|
+
source-destination mappings. The mapping is bidirectional, so it can be queried
|
|
375
|
+
by source or destination paths. All paths manipulated by this class must be in
|
|
376
|
+
relative, canonical form (relative to the project or deploy roots, as appropriate).
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
def __init__(self, project_root: Path):
|
|
380
|
+
self._project_root = project_root
|
|
381
|
+
|
|
382
|
+
# All (src,dest) pairs in inserting order, for iterating
|
|
383
|
+
self.__src_dest_pairs: List[Tuple[Path, Path]] = []
|
|
384
|
+
# built-in dict instances are ordered as of Python 3.7
|
|
385
|
+
self.__src_to_dest: Dict[Path, List[Path]] = {}
|
|
386
|
+
self.__dest_to_src: Dict[Path, Optional[Path]] = {}
|
|
387
|
+
|
|
388
|
+
# This dictionary accumulates keys for each directory or file to be created in
|
|
389
|
+
# the deploy root for any artifact mapping rule being processed. This includes
|
|
390
|
+
# children of directories that are copied to the deploy root. Having this
|
|
391
|
+
# information available is critical to detect possible clashes between rules.
|
|
392
|
+
self._dest_is_dir: Dict[Path, bool] = {}
|
|
393
|
+
|
|
394
|
+
def put(self, src: Path, dest: Path, dest_is_dir: bool) -> None:
|
|
395
|
+
"""
|
|
396
|
+
Adds a new source-destination mapping pair to this map, if necessary. Note that
|
|
397
|
+
this is internal logic that assumes that src-dest pairs have already been preprocessed
|
|
398
|
+
by the enclosing BundleMap (for example, only file -> file and
|
|
399
|
+
directory -> directory mappings are possible here due to the preprocessing step).
|
|
400
|
+
|
|
401
|
+
Arguments:
|
|
402
|
+
src {Path} -- the source path, in canonical form.
|
|
403
|
+
dest {Path} -- the destination path, in canonical form.
|
|
404
|
+
dest_is_dir {bool} -- whether the destination path is a directory.
|
|
405
|
+
"""
|
|
406
|
+
# Both paths should be in canonical form
|
|
407
|
+
assert not src.is_absolute()
|
|
408
|
+
assert not dest.is_absolute()
|
|
409
|
+
|
|
410
|
+
absolute_src = self._project_root / src
|
|
411
|
+
|
|
412
|
+
current_source = self.__dest_to_src.get(dest)
|
|
413
|
+
src_is_dir = absolute_src.is_dir()
|
|
414
|
+
if dest_is_dir:
|
|
415
|
+
assert src_is_dir # file -> directory is not possible here given how rules are processed
|
|
416
|
+
|
|
417
|
+
# directory -> directory
|
|
418
|
+
# Check that dest is currently unmapped
|
|
419
|
+
current_is_dir = self._dest_is_dir.get(dest, False)
|
|
420
|
+
if current_is_dir:
|
|
421
|
+
# mapping to an existing directory is not allowed
|
|
422
|
+
raise TooManyFilesError(dest)
|
|
423
|
+
else:
|
|
424
|
+
# file -> file
|
|
425
|
+
# Check that there is no previous mapping for the same file.
|
|
426
|
+
if current_source is not None and current_source != src:
|
|
427
|
+
# There is already a different source mapping to this destination
|
|
428
|
+
raise TooManyFilesError(dest)
|
|
429
|
+
|
|
430
|
+
if src_is_dir:
|
|
431
|
+
# mark all subdirectories of this source as directories so that we can
|
|
432
|
+
# detect accidental clobbering
|
|
433
|
+
for root, _, files in os.walk(absolute_src, followlinks=True):
|
|
434
|
+
canonical_subdir = Path(root).relative_to(absolute_src)
|
|
435
|
+
canonical_dest_subdir = dest / canonical_subdir
|
|
436
|
+
self._update_dest_is_dir(canonical_dest_subdir, is_dir=True)
|
|
437
|
+
for f in files:
|
|
438
|
+
self._update_dest_is_dir(canonical_dest_subdir / f, is_dir=False)
|
|
439
|
+
|
|
440
|
+
# make sure we check for dest_is_dir consistency regardless of whether the
|
|
441
|
+
# insertion happened. This update can fail, so we need to do it first to
|
|
442
|
+
# avoid applying partial updates to the underlying data storage.
|
|
443
|
+
self._update_dest_is_dir(dest, dest_is_dir)
|
|
444
|
+
|
|
445
|
+
dests = self.__src_to_dest.setdefault(src, [])
|
|
446
|
+
if dest not in dests:
|
|
447
|
+
dests.append(dest)
|
|
448
|
+
self.__dest_to_src[dest] = src
|
|
449
|
+
self.__src_dest_pairs.append((src, dest))
|
|
450
|
+
|
|
451
|
+
def get_source(self, dest: Path) -> Optional[Path]:
|
|
452
|
+
"""
|
|
453
|
+
Returns the source path associated with the provided destination path, if any.
|
|
454
|
+
"""
|
|
455
|
+
return self.__dest_to_src.get(dest)
|
|
456
|
+
|
|
457
|
+
def get_destinations(self, src: Path) -> Iterable[Path]:
|
|
458
|
+
"""
|
|
459
|
+
Returns all destination paths associated with the provided source path, in insertion order.
|
|
460
|
+
"""
|
|
461
|
+
return self.__src_to_dest.get(src, [])
|
|
462
|
+
|
|
463
|
+
def all_sources(self) -> Iterable[Path]:
|
|
464
|
+
"""
|
|
465
|
+
Returns all source paths associated with this map, in insertion order.
|
|
466
|
+
"""
|
|
467
|
+
return self.__src_to_dest.keys()
|
|
468
|
+
|
|
469
|
+
def is_empty(self) -> bool:
|
|
470
|
+
"""
|
|
471
|
+
Returns True if this map has no source-destination mappings.
|
|
472
|
+
"""
|
|
473
|
+
return len(self.__src_dest_pairs) == 0
|
|
474
|
+
|
|
475
|
+
def __iter__(self) -> Iterator[Tuple[Path, Path]]:
|
|
476
|
+
"""
|
|
477
|
+
Returns all (source, destination) pairs known to this map, in insertion order.
|
|
478
|
+
"""
|
|
479
|
+
return iter(self.__src_dest_pairs)
|
|
480
|
+
|
|
481
|
+
def _update_dest_is_dir(self, dest: Path, is_dir: bool) -> None:
|
|
482
|
+
"""
|
|
483
|
+
Recursively marks seen destination paths as either files or folders, raising an error if any inconsistencies
|
|
484
|
+
from previous invocations of this method are encountered.
|
|
485
|
+
|
|
486
|
+
Arguments:
|
|
487
|
+
dest {Path} -- the destination path, in canonical form.
|
|
488
|
+
is_dir {bool} -- whether the destination path is a directory.
|
|
489
|
+
"""
|
|
490
|
+
assert not dest.is_absolute() # dest must be in canonical relative form
|
|
491
|
+
|
|
492
|
+
current_is_dir = self._dest_is_dir.get(dest, None)
|
|
493
|
+
if current_is_dir is not None and current_is_dir != is_dir:
|
|
494
|
+
raise ArtifactError(f"Conflicting type for destination path: {dest}")
|
|
495
|
+
|
|
496
|
+
parent = dest.parent
|
|
497
|
+
if parent != dest:
|
|
498
|
+
self._update_dest_is_dir(parent, True)
|
|
499
|
+
|
|
500
|
+
self._dest_is_dir[dest] = is_dir
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from click import ClickException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeployRootError(ClickException):
|
|
11
|
+
"""
|
|
12
|
+
The deploy root was incorrectly specified.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, msg: str):
|
|
16
|
+
super().__init__(msg)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArtifactError(ClickException):
|
|
20
|
+
"""
|
|
21
|
+
Could not parse source or destination artifact.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, msg: str):
|
|
25
|
+
super().__init__(msg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SourceNotFoundError(ClickException):
|
|
29
|
+
"""
|
|
30
|
+
No match was found for the specified source in the project directory
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, src: Union[str, Path]):
|
|
34
|
+
super().__init__(f"{dedent(str(self.__doc__))}: {src}".strip())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TooManyFilesError(ClickException):
|
|
38
|
+
"""
|
|
39
|
+
Multiple file or directories were mapped to one output destination.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
dest_path: Path
|
|
43
|
+
|
|
44
|
+
def __init__(self, dest_path: Path):
|
|
45
|
+
super().__init__(
|
|
46
|
+
f"{dedent(str(self.__doc__))}\ndestination = {dest_path}".strip()
|
|
47
|
+
)
|
|
48
|
+
self.dest_path = dest_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NotInDeployRootError(ClickException):
|
|
52
|
+
"""
|
|
53
|
+
The specified destination path is outside of the deploy root, or
|
|
54
|
+
would entirely replace it. This can happen when a relative path
|
|
55
|
+
with ".." is provided, or when "." is used as the destination
|
|
56
|
+
(use "./" instead to copy into the deploy root).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
dest_path: Union[str, Path]
|
|
60
|
+
deploy_root: Path
|
|
61
|
+
src_path: Optional[Union[str, Path]]
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
dest_path: Union[Path, str],
|
|
67
|
+
deploy_root: Path,
|
|
68
|
+
src_path: Optional[Union[str, Path]] = None,
|
|
69
|
+
):
|
|
70
|
+
message = dedent(str(self.__doc__))
|
|
71
|
+
message += f"\ndestination = {dest_path}"
|
|
72
|
+
message += f"\ndeploy root = {deploy_root}"
|
|
73
|
+
if src_path is not None:
|
|
74
|
+
message += f"""\nsource = {src_path}"""
|
|
75
|
+
super().__init__(message.strip())
|
|
76
|
+
self.dest_path = dest_path
|
|
77
|
+
self.deploy_root = deploy_root
|
|
78
|
+
self.src_path = src_path
|
|
@@ -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
|
+
)
|