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.
Files changed (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/__init__.py +2 -0
  3. snowflake/cli/api/cli_global_context.py +8 -1
  4. snowflake/cli/api/commands/decorators.py +2 -2
  5. snowflake/cli/api/commands/flags.py +49 -4
  6. snowflake/cli/api/commands/snow_typer.py +2 -0
  7. snowflake/cli/api/console/abc.py +2 -0
  8. snowflake/cli/api/console/console.py +6 -5
  9. snowflake/cli/api/constants.py +5 -0
  10. snowflake/cli/api/exceptions.py +12 -0
  11. snowflake/cli/api/identifiers.py +123 -0
  12. snowflake/cli/api/plugins/command/__init__.py +2 -0
  13. snowflake/cli/api/plugins/plugin_config.py +2 -0
  14. snowflake/cli/api/project/definition.py +2 -0
  15. snowflake/cli/api/project/errors.py +3 -3
  16. snowflake/cli/api/project/schemas/identifier_model.py +35 -0
  17. snowflake/cli/api/project/schemas/native_app/native_app.py +4 -0
  18. snowflake/cli/api/project/schemas/native_app/path_mapping.py +21 -3
  19. snowflake/cli/api/project/schemas/project_definition.py +58 -6
  20. snowflake/cli/api/project/schemas/snowpark/argument.py +2 -0
  21. snowflake/cli/api/project/schemas/snowpark/callable.py +8 -17
  22. snowflake/cli/api/project/schemas/streamlit/streamlit.py +2 -2
  23. snowflake/cli/api/project/schemas/updatable_model.py +2 -0
  24. snowflake/cli/api/project/util.py +2 -0
  25. snowflake/cli/api/secure_path.py +2 -0
  26. snowflake/cli/api/sql_execution.py +14 -54
  27. snowflake/cli/api/utils/cursor.py +2 -0
  28. snowflake/cli/api/utils/models.py +23 -0
  29. snowflake/cli/api/utils/naming_utils.py +0 -27
  30. snowflake/cli/api/utils/rendering.py +178 -23
  31. snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +2 -0
  32. snowflake/cli/app/cli_app.py +4 -1
  33. snowflake/cli/app/commands_registration/builtin_plugins.py +8 -0
  34. snowflake/cli/app/commands_registration/command_plugins_loader.py +2 -0
  35. snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +2 -0
  36. snowflake/cli/app/commands_registration/typer_registration.py +2 -0
  37. snowflake/cli/app/dev/pycharm_remote_debug.py +2 -0
  38. snowflake/cli/app/loggers.py +2 -0
  39. snowflake/cli/app/main_typer.py +1 -1
  40. snowflake/cli/app/printing.py +3 -1
  41. snowflake/cli/app/snow_connector.py +2 -2
  42. snowflake/cli/plugins/connection/commands.py +5 -14
  43. snowflake/cli/plugins/connection/util.py +1 -1
  44. snowflake/cli/plugins/cortex/__init__.py +0 -0
  45. snowflake/cli/plugins/cortex/commands.py +312 -0
  46. snowflake/cli/plugins/cortex/constants.py +3 -0
  47. snowflake/cli/plugins/cortex/manager.py +175 -0
  48. snowflake/cli/plugins/cortex/plugin_spec.py +16 -0
  49. snowflake/cli/plugins/cortex/types.py +8 -0
  50. snowflake/cli/plugins/git/commands.py +15 -0
  51. snowflake/cli/plugins/nativeapp/artifacts.py +368 -123
  52. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +45 -0
  53. snowflake/cli/plugins/nativeapp/codegen/compiler.py +104 -0
  54. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +2 -0
  55. snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +181 -0
  56. snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +196 -0
  57. snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +47 -0
  58. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +489 -0
  59. snowflake/cli/plugins/nativeapp/commands.py +11 -4
  60. snowflake/cli/plugins/nativeapp/common_flags.py +12 -5
  61. snowflake/cli/plugins/nativeapp/constants.py +1 -0
  62. snowflake/cli/plugins/nativeapp/manager.py +49 -16
  63. snowflake/cli/plugins/nativeapp/policy.py +2 -0
  64. snowflake/cli/plugins/nativeapp/run_processor.py +28 -10
  65. snowflake/cli/plugins/nativeapp/teardown_processor.py +80 -8
  66. snowflake/cli/plugins/nativeapp/utils.py +7 -6
  67. snowflake/cli/plugins/nativeapp/version/commands.py +6 -5
  68. snowflake/cli/plugins/nativeapp/version/version_processor.py +2 -0
  69. snowflake/cli/plugins/notebook/commands.py +21 -0
  70. snowflake/cli/plugins/notebook/exceptions.py +6 -0
  71. snowflake/cli/plugins/notebook/manager.py +46 -3
  72. snowflake/cli/plugins/notebook/types.py +2 -0
  73. snowflake/cli/plugins/object/command_aliases.py +80 -0
  74. snowflake/cli/plugins/object/commands.py +10 -6
  75. snowflake/cli/plugins/object/common.py +2 -0
  76. snowflake/cli/plugins/object_stage_deprecated/__init__.py +1 -0
  77. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +20 -0
  78. snowflake/cli/plugins/snowpark/commands.py +62 -6
  79. snowflake/cli/plugins/snowpark/common.py +17 -6
  80. snowflake/cli/plugins/spcs/compute_pool/commands.py +22 -1
  81. snowflake/cli/plugins/spcs/compute_pool/manager.py +2 -0
  82. snowflake/cli/plugins/spcs/image_repository/commands.py +25 -1
  83. snowflake/cli/plugins/spcs/image_repository/manager.py +3 -1
  84. snowflake/cli/plugins/spcs/services/commands.py +39 -5
  85. snowflake/cli/plugins/spcs/services/manager.py +2 -0
  86. snowflake/cli/plugins/sql/commands.py +13 -5
  87. snowflake/cli/plugins/sql/manager.py +40 -19
  88. snowflake/cli/plugins/stage/commands.py +29 -3
  89. snowflake/cli/plugins/stage/diff.py +2 -0
  90. snowflake/cli/plugins/streamlit/commands.py +26 -10
  91. snowflake/cli/plugins/streamlit/manager.py +9 -10
  92. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/METADATA +4 -2
  93. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/RECORD +97 -77
  94. /snowflake/cli/plugins/{object/stage_deprecated → object_stage_deprecated}/commands.py +0 -0
  95. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/WHEEL +0 -0
  96. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0.dist-info}/entry_points.txt +0 -0
  97. {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 typing import Dict, List, Optional, Tuple, Union
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
- The specifically-referenced source file or directory was not found
46
- in the project directory.
37
+ No match was found for the specified source in the project directory
47
38
  """
48
39
 
49
- path: Path
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__(f"{self.__doc__}\ndest_path = {dest_path}")
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
- artifact_src: str
77
- dest_path: Path
66
+ dest_path: Union[str, Path]
78
67
  deploy_root: Path
79
-
80
- def __init__(self, artifact_src: str, dest_path: Path, deploy_root: Path):
81
- super().__init__(
82
- f"""
83
- {self.__doc__}
84
- artifact_src = {artifact_src}
85
- dest_path = {dest_path}
86
- deploy_root = {deploy_root}
87
- """
88
- )
89
- self.artifact_src = artifact_src
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
- def is_glob(s: str) -> bool:
106
- return "*" in s
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(src: Path, dst: Path, makedirs=True, overwrite=True) -> None:
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[dict, str]) -> ArtifactMapping:
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
- ) -> ArtifactDeploymentMap:
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
- mapped_files: ArtifactDeploymentMap = {}
484
+ bundle_map = BundleMap(project_root=project_root, deploy_root=deploy_root)
227
485
  for artifact in artifacts:
228
- dest_path = resolve_without_follow(Path(resolved_root, artifact.dest))
229
- source_paths = get_source_paths(artifact, project_root)
230
-
231
- if specifies_directory(artifact.dest):
232
- # make sure we are only modifying files / directories inside the deploy root
233
- if resolved_root != dest_path and resolved_root not in dest_path.parents:
234
- raise NotInDeployRootError(artifact.src, dest_path, resolved_root)
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
- manifest_file = find_manifest_file(deploy_root)
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