snowflake-cli-labs 2.4.1__py3-none-any.whl → 2.5.0rc1__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 (204) hide show
  1. snowflake/cli/__about__.py +15 -1
  2. snowflake/cli/__init__.py +13 -0
  3. snowflake/cli/api/__init__.py +14 -0
  4. snowflake/cli/api/cli_global_context.py +26 -0
  5. snowflake/cli/api/commands/__init__.py +13 -0
  6. snowflake/cli/api/commands/alias.py +14 -0
  7. snowflake/cli/api/commands/decorators.py +14 -0
  8. snowflake/cli/api/commands/experimental_behaviour.py +14 -0
  9. snowflake/cli/api/commands/flags.py +21 -1
  10. snowflake/cli/api/commands/project_initialisation.py +19 -2
  11. snowflake/cli/api/commands/snow_typer.py +90 -1
  12. snowflake/cli/api/config.py +14 -0
  13. snowflake/cli/api/console/__init__.py +14 -0
  14. snowflake/cli/api/console/abc.py +14 -0
  15. snowflake/cli/api/console/console.py +14 -0
  16. snowflake/cli/api/console/enum.py +14 -0
  17. snowflake/cli/api/constants.py +14 -0
  18. snowflake/cli/api/exceptions.py +22 -0
  19. snowflake/cli/api/feature_flags.py +14 -1
  20. snowflake/cli/api/identifiers.py +16 -2
  21. snowflake/cli/api/output/__init__.py +13 -0
  22. snowflake/cli/api/output/formats.py +14 -0
  23. snowflake/cli/api/output/types.py +14 -0
  24. snowflake/cli/api/plugins/__init__.py +13 -0
  25. snowflake/cli/api/plugins/command/__init__.py +14 -0
  26. snowflake/cli/api/plugins/command/plugin_hook_specs.py +14 -0
  27. snowflake/cli/api/plugins/plugin_config.py +14 -0
  28. snowflake/cli/api/project/__init__.py +13 -0
  29. snowflake/cli/api/project/definition.py +54 -8
  30. snowflake/cli/api/project/definition_manager.py +28 -2
  31. snowflake/cli/api/project/errors.py +14 -0
  32. snowflake/cli/api/project/schemas/__init__.py +13 -0
  33. snowflake/cli/api/project/schemas/identifier_model.py +14 -0
  34. snowflake/cli/api/project/schemas/native_app/__init__.py +13 -0
  35. snowflake/cli/api/project/schemas/native_app/application.py +14 -0
  36. snowflake/cli/api/project/schemas/native_app/native_app.py +35 -0
  37. snowflake/cli/api/project/schemas/native_app/package.py +14 -0
  38. snowflake/cli/api/project/schemas/native_app/path_mapping.py +15 -1
  39. snowflake/cli/api/project/schemas/project_definition.py +14 -0
  40. snowflake/cli/api/project/schemas/snowpark/__init__.py +13 -0
  41. snowflake/cli/api/project/schemas/snowpark/argument.py +14 -0
  42. snowflake/cli/api/project/schemas/snowpark/callable.py +14 -0
  43. snowflake/cli/api/project/schemas/snowpark/snowpark.py +14 -0
  44. snowflake/cli/api/project/schemas/streamlit/__init__.py +13 -0
  45. snowflake/cli/api/project/schemas/streamlit/streamlit.py +14 -0
  46. snowflake/cli/api/project/schemas/updatable_model.py +14 -0
  47. snowflake/cli/api/project/util.py +32 -3
  48. snowflake/cli/api/secure_path.py +59 -7
  49. snowflake/cli/api/secure_utils.py +14 -0
  50. snowflake/cli/api/sql_execution.py +14 -0
  51. snowflake/cli/api/utils/__init__.py +13 -0
  52. snowflake/cli/api/utils/cursor.py +14 -0
  53. snowflake/cli/api/utils/definition_rendering.py +268 -0
  54. snowflake/cli/api/utils/dict_utils.py +73 -0
  55. snowflake/cli/api/utils/error_handling.py +14 -0
  56. snowflake/cli/api/utils/graph.py +97 -0
  57. snowflake/cli/api/utils/models.py +14 -0
  58. snowflake/cli/api/utils/naming_utils.py +13 -0
  59. snowflake/cli/api/utils/path_utils.py +14 -0
  60. snowflake/cli/api/utils/rendering.py +21 -152
  61. snowflake/cli/api/utils/types.py +18 -1
  62. snowflake/cli/app/__init__.py +14 -0
  63. snowflake/cli/app/__main__.py +14 -0
  64. snowflake/cli/app/api_impl/__init__.py +13 -0
  65. snowflake/cli/app/api_impl/plugin/__init__.py +13 -0
  66. snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +14 -0
  67. snowflake/cli/app/cli_app.py +14 -0
  68. snowflake/cli/app/commands_registration/__init__.py +14 -0
  69. snowflake/cli/app/commands_registration/builtin_plugins.py +14 -0
  70. snowflake/cli/app/commands_registration/command_plugins_loader.py +14 -0
  71. snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +14 -0
  72. snowflake/cli/app/commands_registration/exception_logging.py +14 -0
  73. snowflake/cli/app/commands_registration/threadsafe.py +14 -0
  74. snowflake/cli/app/commands_registration/typer_registration.py +14 -0
  75. snowflake/cli/app/constants.py +14 -0
  76. snowflake/cli/app/dev/__init__.py +13 -0
  77. snowflake/cli/app/dev/commands_structure.py +14 -0
  78. snowflake/cli/app/dev/docs/__init__.py +13 -0
  79. snowflake/cli/app/dev/docs/generator.py +14 -0
  80. snowflake/cli/app/dev/pycharm_remote_debug.py +14 -0
  81. snowflake/cli/app/loggers.py +14 -0
  82. snowflake/cli/app/main_typer.py +14 -0
  83. snowflake/cli/app/printing.py +14 -0
  84. snowflake/cli/app/snow_connector.py +14 -0
  85. snowflake/cli/app/telemetry.py +14 -0
  86. snowflake/cli/plugins/__init__.py +13 -0
  87. snowflake/cli/plugins/connection/__init__.py +13 -0
  88. snowflake/cli/plugins/connection/commands.py +27 -2
  89. snowflake/cli/plugins/connection/plugin_spec.py +15 -1
  90. snowflake/cli/plugins/connection/util.py +21 -1
  91. snowflake/cli/plugins/cortex/__init__.py +13 -0
  92. snowflake/cli/plugins/cortex/commands.py +16 -2
  93. snowflake/cli/plugins/cortex/constants.py +14 -0
  94. snowflake/cli/plugins/cortex/manager.py +14 -0
  95. snowflake/cli/plugins/cortex/plugin_spec.py +15 -1
  96. snowflake/cli/plugins/cortex/types.py +14 -0
  97. snowflake/cli/plugins/git/__init__.py +13 -0
  98. snowflake/cli/plugins/git/commands.py +16 -2
  99. snowflake/cli/plugins/git/manager.py +14 -0
  100. snowflake/cli/plugins/git/plugin_spec.py +15 -1
  101. snowflake/cli/plugins/nativeapp/__init__.py +13 -0
  102. snowflake/cli/plugins/nativeapp/artifacts.py +202 -98
  103. snowflake/cli/plugins/nativeapp/codegen/__init__.py +13 -0
  104. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +14 -0
  105. snowflake/cli/plugins/nativeapp/codegen/compiler.py +14 -0
  106. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +14 -0
  107. snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +5 -5
  108. snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +34 -13
  109. snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +14 -0
  110. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +52 -13
  111. snowflake/cli/plugins/nativeapp/commands.py +63 -10
  112. snowflake/cli/plugins/nativeapp/common_flags.py +20 -0
  113. snowflake/cli/plugins/nativeapp/constants.py +15 -0
  114. snowflake/cli/plugins/nativeapp/exceptions.py +23 -2
  115. snowflake/cli/plugins/nativeapp/init.py +14 -0
  116. snowflake/cli/plugins/nativeapp/manager.py +118 -25
  117. snowflake/cli/plugins/nativeapp/plugin_spec.py +15 -1
  118. snowflake/cli/plugins/nativeapp/policy.py +14 -0
  119. snowflake/cli/plugins/nativeapp/run_processor.py +22 -3
  120. snowflake/cli/plugins/nativeapp/teardown_processor.py +14 -0
  121. snowflake/cli/plugins/nativeapp/utils.py +14 -0
  122. snowflake/cli/plugins/nativeapp/version/__init__.py +13 -0
  123. snowflake/cli/plugins/nativeapp/version/commands.py +19 -4
  124. snowflake/cli/plugins/nativeapp/version/version_processor.py +26 -4
  125. snowflake/cli/plugins/notebook/__init__.py +13 -0
  126. snowflake/cli/plugins/notebook/commands.py +16 -4
  127. snowflake/cli/plugins/notebook/exceptions.py +14 -0
  128. snowflake/cli/plugins/notebook/manager.py +14 -0
  129. snowflake/cli/plugins/notebook/plugin_spec.py +15 -1
  130. snowflake/cli/plugins/notebook/types.py +14 -0
  131. snowflake/cli/plugins/object/__init__.py +13 -11
  132. snowflake/cli/plugins/object/command_aliases.py +20 -6
  133. snowflake/cli/plugins/object/commands.py +16 -2
  134. snowflake/cli/plugins/object/common.py +14 -0
  135. snowflake/cli/plugins/object/manager.py +14 -0
  136. snowflake/cli/plugins/object/plugin_spec.py +16 -2
  137. snowflake/cli/plugins/object_stage_deprecated/__init__.py +14 -0
  138. snowflake/cli/plugins/object_stage_deprecated/commands.py +16 -2
  139. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +16 -4
  140. snowflake/cli/plugins/snowpark/__init__.py +13 -4
  141. snowflake/cli/plugins/snowpark/commands.py +18 -2
  142. snowflake/cli/plugins/snowpark/common.py +15 -0
  143. snowflake/cli/plugins/snowpark/manager.py +14 -0
  144. snowflake/cli/plugins/snowpark/models.py +15 -1
  145. snowflake/cli/plugins/snowpark/package/__init__.py +13 -0
  146. snowflake/cli/plugins/snowpark/package/anaconda_packages.py +14 -0
  147. snowflake/cli/plugins/snowpark/package/commands.py +16 -2
  148. snowflake/cli/plugins/snowpark/package/manager.py +14 -0
  149. snowflake/cli/plugins/snowpark/package/utils.py +14 -0
  150. snowflake/cli/plugins/snowpark/package_utils.py +15 -1
  151. snowflake/cli/plugins/snowpark/plugin_spec.py +16 -2
  152. snowflake/cli/plugins/snowpark/snowpark_package_paths.py +14 -0
  153. snowflake/cli/plugins/snowpark/snowpark_shared.py +14 -0
  154. snowflake/cli/plugins/snowpark/zipper.py +20 -3
  155. snowflake/cli/plugins/spcs/__init__.py +16 -2
  156. snowflake/cli/plugins/spcs/common.py +14 -0
  157. snowflake/cli/plugins/spcs/compute_pool/__init__.py +13 -0
  158. snowflake/cli/plugins/spcs/compute_pool/commands.py +16 -2
  159. snowflake/cli/plugins/spcs/compute_pool/manager.py +14 -0
  160. snowflake/cli/plugins/spcs/image_registry/__init__.py +13 -0
  161. snowflake/cli/plugins/spcs/image_registry/commands.py +16 -2
  162. snowflake/cli/plugins/spcs/image_registry/manager.py +16 -0
  163. snowflake/cli/plugins/spcs/image_repository/__init__.py +13 -0
  164. snowflake/cli/plugins/spcs/image_repository/commands.py +16 -2
  165. snowflake/cli/plugins/spcs/image_repository/manager.py +14 -0
  166. snowflake/cli/plugins/spcs/jobs/__init__.py +13 -0
  167. snowflake/cli/plugins/spcs/jobs/commands.py +17 -3
  168. snowflake/cli/plugins/spcs/jobs/manager.py +14 -0
  169. snowflake/cli/plugins/spcs/plugin_spec.py +15 -1
  170. snowflake/cli/plugins/spcs/services/__init__.py +13 -0
  171. snowflake/cli/plugins/spcs/services/commands.py +16 -2
  172. snowflake/cli/plugins/spcs/services/manager.py +14 -0
  173. snowflake/cli/plugins/sql/__init__.py +13 -0
  174. snowflake/cli/plugins/sql/commands.py +16 -2
  175. snowflake/cli/plugins/sql/manager.py +14 -0
  176. snowflake/cli/plugins/sql/plugin_spec.py +15 -1
  177. snowflake/cli/plugins/sql/snowsql_templating.py +14 -0
  178. snowflake/cli/plugins/stage/__init__.py +13 -0
  179. snowflake/cli/plugins/stage/commands.py +17 -3
  180. snowflake/cli/plugins/stage/diff.py +14 -0
  181. snowflake/cli/plugins/stage/manager.py +16 -5
  182. snowflake/cli/plugins/stage/plugin_spec.py +16 -2
  183. snowflake/cli/plugins/streamlit/__init__.py +13 -0
  184. snowflake/cli/plugins/streamlit/commands.py +16 -2
  185. snowflake/cli/plugins/streamlit/manager.py +14 -0
  186. snowflake/cli/plugins/streamlit/plugin_spec.py +15 -1
  187. snowflake/cli/templates/default_snowpark/app/__init__.py +13 -0
  188. snowflake/cli/templates/default_snowpark/app/common.py +15 -0
  189. snowflake/cli/templates/default_snowpark/app/functions.py +14 -0
  190. snowflake/cli/templates/default_snowpark/app/procedures.py +14 -0
  191. snowflake/cli/templates/default_snowpark/snowflake.yml +1 -1
  192. snowflake/cli/templates/default_streamlit/common/hello.py +15 -0
  193. snowflake/cli/templates/default_streamlit/pages/my_page.py +14 -0
  194. snowflake/cli/templates/default_streamlit/snowflake.yml +1 -1
  195. snowflake/cli/templates/default_streamlit/streamlit_app.py +14 -0
  196. {snowflake_cli_labs-2.4.1.dist-info → snowflake_cli_labs-2.5.0rc1.dist-info}/METADATA +23 -7
  197. snowflake_cli_labs-2.5.0rc1.dist-info/RECORD +206 -0
  198. snowflake/cli/plugins/nativeapp/feature_flags.py +0 -10
  199. snowflake/cli/templates/environment.yml.jinja +0 -5
  200. snowflake/cli/templates/streamlit_app_launcher.py.jinja +0 -19
  201. snowflake_cli_labs-2.4.1.dist-info/RECORD +0 -206
  202. {snowflake_cli_labs-2.4.1.dist-info → snowflake_cli_labs-2.5.0rc1.dist-info}/WHEEL +0 -0
  203. {snowflake_cli_labs-2.4.1.dist-info → snowflake_cli_labs-2.5.0rc1.dist-info}/entry_points.txt +0 -0
  204. {snowflake_cli_labs-2.4.1.dist-info → snowflake_cli_labs-2.5.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  from __future__ import annotations
2
16
 
3
17
  import errno
@@ -50,7 +64,7 @@ class SecurePath:
50
64
  """
51
65
  When the path points to a directory, yield path objects of the directory contents.
52
66
  Otherwise, NotADirectoryError is raised.
53
- If the locartion does not exists, FileNotFoundError is raised.
67
+ If the location does not exist, FileNotFoundError is raised.
54
68
 
55
69
  For details, check pathlib.Path.iterdir()
56
70
  """
@@ -64,6 +78,25 @@ class SecurePath:
64
78
  """
65
79
  return self._path.exists()
66
80
 
81
+ def is_dir(self) -> bool:
82
+ """
83
+ Return True if the path points to a directory (or a symbolic link pointing to a directory),
84
+ False if it points to another kind of file.
85
+ """
86
+ return self._path.is_dir()
87
+
88
+ def is_file(self) -> bool:
89
+ """
90
+ Return True if the path points to a regular file (or a symbolic link pointing to a regular file),
91
+ False if it points to another kind of file.
92
+ """
93
+ return self._path.is_file()
94
+
95
+ @property
96
+ def name(self) -> str:
97
+ """A string representing the final path component."""
98
+ return self._path.name
99
+
67
100
  def chmod(self, permissions_mask: int) -> None:
68
101
  """
69
102
  Change the file mode and permissions, like os.chmod().
@@ -73,6 +106,20 @@ class SecurePath:
73
106
  )
74
107
  self._path.chmod(permissions_mask)
75
108
 
109
+ def restrict_permissions(self) -> None:
110
+ """
111
+ Restrict file/directory permissions to owner-only.
112
+ """
113
+ import stat
114
+
115
+ owner_permissions = (
116
+ # https://docs.python.org/3/library/stat.html
117
+ stat.S_IRUSR # readable by owner
118
+ | stat.S_IWUSR # writeable by owner
119
+ | stat.S_IXUSR # executable by owner
120
+ )
121
+ self.chmod(self._path.stat().st_mode & owner_permissions)
122
+
76
123
  def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
77
124
  """
78
125
  Create a file at this given path. For details, check pathlib.Path.touch()
@@ -90,9 +137,13 @@ class SecurePath:
90
137
  """
91
138
  Create a directory at this given path. For details, check pathlib.Path.mkdir()
92
139
  """
140
+ if parents and not self.parent.exists():
141
+ self.parent.mkdir(
142
+ permissions_mask=permissions_mask, exist_ok=exist_ok, parents=True
143
+ )
93
144
  if not self.exists():
94
145
  log.info("Creating directory %s", str(self._path))
95
- self._path.mkdir(mode=permissions_mask, parents=parents, exist_ok=exist_ok)
146
+ self._path.mkdir(mode=permissions_mask, exist_ok=exist_ok)
96
147
 
97
148
  def read_text(self, file_size_limit_mb: int, *args, **kwargs) -> str:
98
149
  """
@@ -203,19 +254,20 @@ class SecurePath:
203
254
  for child in src.iterdir():
204
255
  _recursive_check_for_conflicts(child, dst / child.name)
205
256
 
206
- def _recursive_copy(src: Path, dst: Path):
257
+ def _recursive_copy(src: SecurePath, dst: SecurePath):
207
258
  if src.is_file():
208
- log.info("Copying file %s into %s", src, dst)
259
+ log.info("Copying file %s into %s", src.path, dst.path)
209
260
  if dst.exists():
210
261
  dst.unlink()
211
- shutil.copyfile(src, dst)
262
+ shutil.copyfile(src.path, dst.path)
263
+ dst.restrict_permissions()
212
264
  if src.is_dir():
213
- self.__class__(dst).mkdir(exist_ok=True)
265
+ dst.mkdir(exist_ok=True)
214
266
  for child in src.iterdir():
215
267
  _recursive_copy(child, dst / child.name)
216
268
 
217
269
  _recursive_check_for_conflicts(self._path, destination)
218
- _recursive_copy(self._path, destination)
270
+ _recursive_copy(self, self.__class__(destination))
219
271
 
220
272
  return SecurePath(destination)
221
273
 
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  import stat
2
16
  from pathlib import Path
3
17
 
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  from __future__ import annotations
2
16
 
3
17
  import logging
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  from __future__ import annotations
2
16
 
3
17
  from typing import Callable, List, Optional
@@ -0,0 +1,268 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import copy
18
+ import os
19
+ from typing import Any, Optional
20
+
21
+ from jinja2 import Environment, UndefinedError, nodes
22
+ from packaging.version import Version
23
+ from snowflake.cli.api.exceptions import CycleDetectedError, InvalidTemplate
24
+ from snowflake.cli.api.utils.dict_utils import deep_merge_dicts, traverse
25
+ from snowflake.cli.api.utils.graph import Graph, Node
26
+ from snowflake.cli.api.utils.rendering import CONTEXT_KEY, get_snowflake_cli_jinja_env
27
+ from snowflake.cli.api.utils.types import Context, Definition
28
+
29
+
30
+ class TemplatedEnvironment:
31
+ """
32
+ This class is a utility class
33
+ that encapsulates some of the Jinja Templating functionality.
34
+ """
35
+
36
+ def __init__(self, env: Environment):
37
+ self._jinja_env: Environment = env
38
+
39
+ def render(self, template_value: Any, context: Context) -> Any:
40
+ if not self.get_referenced_vars(template_value):
41
+ return template_value
42
+
43
+ jinja_template = self._jinja_env.from_string(str(template_value))
44
+ return jinja_template.render(context)
45
+
46
+ def get_referenced_vars(self, template_value: Any) -> set[TemplateVar]:
47
+ template_str = str(template_value)
48
+ ast = self._jinja_env.parse(template_str)
49
+ return self._get_referenced_vars(ast, template_str)
50
+
51
+ def _get_referenced_vars(
52
+ self,
53
+ ast_node: nodes.Template,
54
+ template_value: str,
55
+ current_attr_chain: Optional[list[str]] = None,
56
+ ) -> set[TemplateVar]:
57
+ """
58
+ Traverse Jinja AST to find the variable chain referenced by the template.
59
+ A variable like ctx.env.test is internally represented in the AST tree as
60
+ Getattr Node (attr='test') -> Getattr Node (attr='env') -> Name Node (name='ctx')
61
+ """
62
+ all_referenced_vars: set[TemplateVar] = set()
63
+ if isinstance(ast_node, nodes.Getattr):
64
+ current_attr_chain = [ast_node.attr] + (current_attr_chain or []) # type: ignore[attr-defined]
65
+ elif isinstance(ast_node, nodes.Name):
66
+ current_attr_chain = [ast_node.name] + (current_attr_chain or []) # type: ignore[attr-defined]
67
+ all_referenced_vars.add(TemplateVar(current_attr_chain))
68
+ current_attr_chain = None
69
+ elif (
70
+ not isinstance(ast_node, (nodes.Template, nodes.TemplateData, nodes.Output))
71
+ or current_attr_chain is not None
72
+ ):
73
+ raise InvalidTemplate(f"Unexpected templating syntax in {template_value}")
74
+
75
+ for child_node in ast_node.iter_child_nodes():
76
+ all_referenced_vars.update(
77
+ self._get_referenced_vars(
78
+ child_node, template_value, current_attr_chain
79
+ )
80
+ )
81
+
82
+ return all_referenced_vars
83
+
84
+
85
+ class TemplateVar:
86
+ """
87
+ This class tracks template variable information.
88
+ For a variable like ctx.env.var, this class will track
89
+ the chain of keys referenced by this variable (ctx, env, var),
90
+ as well as the value of this variable. (e.g. ctx.env.var = "hello_<% ctx.definition_version %>")
91
+
92
+ The value of this variable is divided into 2 parts.
93
+ The templated value (e.g. "hello_<% ctx.definition %>"),
94
+ as well as the rendered_value (e.g. "hello_1.1")
95
+ """
96
+
97
+ def __init__(self, vars_chain):
98
+ self._vars_chain: list[str] = list(vars_chain)
99
+ self.templated_value: Optional[Any] = None
100
+ self.rendered_value: Optional[Any] = None
101
+
102
+ @property
103
+ def key(self) -> str:
104
+ return ".".join(self._vars_chain)
105
+
106
+ @property
107
+ def is_env_var(self) -> bool:
108
+ return (
109
+ len(self._vars_chain) == 3
110
+ and self._vars_chain[0] == CONTEXT_KEY
111
+ and self._vars_chain[1] == "env"
112
+ )
113
+
114
+ def get_env_var_name(self) -> str:
115
+ if not self.is_env_var:
116
+ raise KeyError(
117
+ f"Referenced variable {self.key} is not an environment variable"
118
+ )
119
+ return self._vars_chain[2]
120
+
121
+ def add_to_context(self, context: Context) -> None:
122
+ """
123
+ Takes a multi-level context dict as input. Modifies the context dict with the rendered value of this variable.
124
+
125
+ If the variable has multi-levels (e.g. ctx.env), recursively traverse the dictionary
126
+ to set the inner level's key to the rendered value of this variable.
127
+
128
+ Example: vars chain contains ['ctx', 'env', 'x'], and context is {}, and rendered_value is 'val'.
129
+ At the end of this call, context content will be: {'ctx': {'env': {'x': 'val'}}}
130
+ """
131
+ current_dict_level = context
132
+ last_element_index = len(self._vars_chain) - 1
133
+ for index, var in enumerate(self._vars_chain):
134
+ if index == last_element_index:
135
+ current_dict_level[var] = self.rendered_value
136
+ else:
137
+ current_dict_level.setdefault(var, {})
138
+ current_dict_level = current_dict_level[var]
139
+
140
+ def read_from_context(self, context: Context) -> Any:
141
+ """
142
+ Takes a multi-level context dict as input.
143
+
144
+ If the variable has multi-levels (e.g. ctx.env), recursively traverse the dictionary
145
+ to find the key that the variable points to.
146
+
147
+ Returns the value in that location.
148
+
149
+ Raise UndefinedError if the variable is None or not found.
150
+ """
151
+ current_dict_level = context
152
+ for key in self._vars_chain:
153
+ if (
154
+ not isinstance(current_dict_level, dict)
155
+ or key not in current_dict_level
156
+ ):
157
+ raise UndefinedError(f"Could not find template variable {self.key}")
158
+ current_dict_level = current_dict_level[key]
159
+
160
+ value = current_dict_level
161
+ if value is None or isinstance(value, (dict, list)):
162
+ raise UndefinedError(
163
+ f"Template variable {self.key} does not contain a valid value"
164
+ )
165
+
166
+ return value
167
+
168
+ def __hash__(self):
169
+ return hash(self.key)
170
+
171
+ def __eq__(self, other):
172
+ return self.key == other.key
173
+
174
+
175
+ def _build_dependency_graph(
176
+ env: TemplatedEnvironment, all_vars: set[TemplateVar], context: Context
177
+ ) -> Graph[TemplateVar]:
178
+ dependencies_graph = Graph[TemplateVar]()
179
+ for variable in all_vars:
180
+ dependencies_graph.add(Node[TemplateVar](key=variable.key, data=variable))
181
+
182
+ for variable in all_vars:
183
+ if variable.is_env_var and variable.get_env_var_name() in os.environ:
184
+ # If variable is found in os.environ, then use the value as is
185
+ # skip rendering by pre-setting the rendered_value attribute
186
+ env_value = os.environ.get(variable.get_env_var_name())
187
+ variable.rendered_value = env_value
188
+ variable.templated_value = env_value
189
+ else:
190
+ variable.templated_value = variable.read_from_context(context)
191
+ dependencies_vars = env.get_referenced_vars(variable.templated_value)
192
+
193
+ for referenced_var in dependencies_vars:
194
+ dependencies_graph.add_directed_edge(variable.key, referenced_var.key)
195
+
196
+ return dependencies_graph
197
+
198
+
199
+ def _render_graph_node(env: TemplatedEnvironment, node: Node[TemplateVar]) -> None:
200
+ if node.data.rendered_value is not None:
201
+ # Do not re-evaluate resolved nodes like env variable nodes
202
+ # which might contain template-like values, or non-string nodes
203
+ return
204
+
205
+ current_context: Context = {}
206
+ for dep_node in node.neighbors:
207
+ dep_node.data.add_to_context(current_context)
208
+
209
+ node.data.rendered_value = env.render(node.data.templated_value, current_context)
210
+
211
+
212
+ def render_definition_template(original_definition: Definition) -> Definition:
213
+ """
214
+ Takes a definition file as input. An arbitrary structure containing dict|list|scalars,
215
+ with the top level being a dictionary.
216
+ Requires item 'definition_version' to be set to a version of 1.1 and higher.
217
+
218
+ Searches for any templating in all of the scalar fields, and attempts to resolve them
219
+ from the definition structure itself or from the environment variable.
220
+
221
+ Environment variables take precedence during the rendering process.
222
+ """
223
+
224
+ # protect input from update
225
+ definition = copy.deepcopy(original_definition)
226
+
227
+ if "definition_version" not in definition or Version(
228
+ definition["definition_version"]
229
+ ) < Version("1.1"):
230
+ return definition
231
+
232
+ template_env = TemplatedEnvironment(get_snowflake_cli_jinja_env())
233
+ project_context = {CONTEXT_KEY: definition}
234
+
235
+ referenced_vars = set()
236
+
237
+ def find_any_template_vars(element):
238
+ referenced_vars.update(template_env.get_referenced_vars(element))
239
+
240
+ traverse(definition, visit_action=find_any_template_vars)
241
+
242
+ dependencies_graph = _build_dependency_graph(
243
+ template_env, referenced_vars, project_context
244
+ )
245
+
246
+ def on_cycle_action(node: Node[TemplateVar]):
247
+ raise CycleDetectedError(
248
+ f"Cycle detected in templating variable {node.data.key}"
249
+ )
250
+
251
+ dependencies_graph.dfs(
252
+ visit_action=lambda node: _render_graph_node(template_env, node),
253
+ on_cycle_action=on_cycle_action,
254
+ )
255
+
256
+ # now that we determined the values of all templated vars,
257
+ # use these resolved values as a fresh context to resolve definition
258
+ final_context: Context = {}
259
+ for node in dependencies_graph.get_all_nodes():
260
+ node.data.add_to_context(final_context)
261
+
262
+ traverse(
263
+ definition,
264
+ update_action=lambda val: template_env.render(val, final_context),
265
+ )
266
+ deep_merge_dicts(definition, {"env": dict(os.environ)})
267
+
268
+ return definition
@@ -0,0 +1,73 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Callable
18
+
19
+
20
+ def deep_merge_dicts(
21
+ original_values: dict[Any, Any], override_values: dict[Any, Any]
22
+ ) -> None:
23
+ """
24
+ Takes 2 dictionaries as input: original and override. The original dictionary is modified.
25
+
26
+ For every key in the override dictionary, override the same key
27
+ in the original dictionary, or create a new one if the key is not present.
28
+
29
+ If the override value and the original value are both dictionaries,
30
+ instead of overriding, this function recursively calls itself to merge the keys of the sub-dictionaries.
31
+ """
32
+ if not isinstance(override_values, dict) or not isinstance(original_values, dict):
33
+ raise ValueError("Arguments are not of type dict")
34
+
35
+ for field, value in override_values.items():
36
+ if (
37
+ field in original_values
38
+ and isinstance(original_values[field], dict)
39
+ and isinstance(value, dict)
40
+ ):
41
+ deep_merge_dicts(original_values[field], value)
42
+ else:
43
+ original_values[field] = value
44
+
45
+
46
+ def traverse(
47
+ element: Any,
48
+ visit_action: Callable[[Any], None] = lambda element: None,
49
+ update_action: Callable[[Any], Any] = lambda element: element,
50
+ ) -> Any:
51
+ """
52
+ Traverse a nested structure (lists, dicts, scalars).
53
+
54
+ On traversal, it allows for actions or updates on each visit.
55
+
56
+ visit_action: caller can provide a function to execute on each scalar element in the structure (leaves of the tree).
57
+ visit_action accepts an element (scalar) as input. Return value is ignored.
58
+
59
+ update_action: caller can provide a function to update each scalar element in the structure.
60
+ update_action accepts an element (scalar) as input, and returns the modified value.
61
+
62
+ """
63
+ if isinstance(element, dict):
64
+ for key, value in element.items():
65
+ element[key] = traverse(value, visit_action, update_action)
66
+ return element
67
+ elif isinstance(element, list):
68
+ for index, value in enumerate(element):
69
+ element[index] = traverse(value, visit_action, update_action)
70
+ return element
71
+ else:
72
+ visit_action(element)
73
+ return update_action(element)
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  from contextlib import contextmanager
2
16
 
3
17
 
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+ from typing import Callable, Generic, TypeVar
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class VisitStatus(Enum):
25
+ VISITING = 1
26
+ VISITED = 2
27
+
28
+
29
+ @dataclass
30
+ class Node(Generic[T]):
31
+ key: str
32
+ data: T
33
+ neighbors: set[Node[T]] = field(default_factory=set)
34
+
35
+ def __hash__(self):
36
+ return hash(self.key)
37
+
38
+ def __eq__(self, other):
39
+ return self.key == other.key
40
+
41
+
42
+ class Graph(Generic[T]):
43
+ def __init__(self):
44
+ self._graph_nodes_map: dict[str, Node[T]] = {}
45
+
46
+ def get(self, key: str) -> Node[T]:
47
+ if key in self._graph_nodes_map:
48
+ return self._graph_nodes_map[key]
49
+ raise KeyError(f"Node with key {key} not found")
50
+
51
+ def get_all_nodes(self) -> set[Node[T]]:
52
+ return set(self._graph_nodes_map.values())
53
+
54
+ def add(self, node: Node[T]) -> None:
55
+ if node.key in self._graph_nodes_map:
56
+ raise KeyError(f"Node key {node.key} already exists")
57
+ self._graph_nodes_map[node.key] = node
58
+
59
+ def add_directed_edge(self, from_node_key: str, to_node_key: str) -> None:
60
+ from_node = self.get(from_node_key)
61
+ to_node = self.get(to_node_key)
62
+ from_node.neighbors.add(to_node)
63
+
64
+ @staticmethod
65
+ def _dfs_visit(
66
+ nodes_status: dict[str, VisitStatus],
67
+ node: Node[T],
68
+ visit_action: Callable[[Node[T]], None],
69
+ on_cycle_action: Callable[[Node[T]], None],
70
+ ) -> None:
71
+ if nodes_status.get(node.key) == VisitStatus.VISITED:
72
+ return
73
+
74
+ nodes_status[node.key] = VisitStatus.VISITING
75
+ for neighbor_node in node.neighbors:
76
+ if nodes_status.get(neighbor_node.key) == VisitStatus.VISITING:
77
+ on_cycle_action(node)
78
+ else:
79
+ Graph._dfs_visit(
80
+ nodes_status, neighbor_node, visit_action, on_cycle_action
81
+ )
82
+
83
+ visit_action(node)
84
+
85
+ nodes_status[node.key] = VisitStatus.VISITED
86
+
87
+ def dfs(
88
+ self,
89
+ visit_action: Callable[[Node[T]], None] = lambda node: None,
90
+ on_cycle_action: Callable[[Node[T]], None] = lambda node: None,
91
+ ) -> None:
92
+ nodes_status: dict[str, VisitStatus] = {}
93
+ for node in self._graph_nodes_map.values():
94
+ Graph._dfs_visit(nodes_status, node, visit_action, on_cycle_action)
95
+
96
+ def __contains__(self, key: str) -> bool:
97
+ return key in self._graph_nodes_map
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
1
15
  from __future__ import annotations
2
16
 
3
17
  import os
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.