snowflake-cli 2.8.2__py3-none-any.whl → 3.0.1__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 (251) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/{app → _app}/__main__.py +1 -1
  3. snowflake/cli/{app → _app}/cli_app.py +22 -13
  4. snowflake/cli/{app → _app}/commands_registration/builtin_plugins.py +15 -19
  5. snowflake/cli/{app → _app}/commands_registration/command_plugins_loader.py +9 -9
  6. snowflake/cli/{app → _app}/commands_registration/commands_registration_with_callbacks.py +4 -4
  7. snowflake/cli/{app → _app}/commands_registration/exception_logging.py +2 -2
  8. snowflake/cli/{app → _app}/commands_registration/typer_registration.py +2 -2
  9. snowflake/cli/{app → _app}/dev/docs/commands_docs_generator.py +30 -12
  10. snowflake/cli/{app → _app}/dev/docs/generator.py +3 -3
  11. snowflake/cli/{app → _app}/dev/docs/project_definition_docs_generator.py +4 -4
  12. snowflake/cli/{app → _app}/dev/docs/templates/usage.rst.jinja2 +14 -4
  13. snowflake/cli/{app → _app}/main_typer.py +2 -2
  14. snowflake/cli/{app → _app}/printing.py +2 -2
  15. snowflake/cli/_app/secret.py +9 -0
  16. snowflake/cli/{app → _app}/snow_connector.py +127 -61
  17. snowflake/cli/{app → _app}/telemetry.py +38 -7
  18. snowflake/cli/_app/version_check.py +74 -0
  19. snowflake/cli/{plugins → _plugins}/connection/commands.py +34 -11
  20. snowflake/cli/_plugins/connection/plugin_spec.py +30 -0
  21. snowflake/cli/{plugins → _plugins}/connection/util.py +16 -0
  22. snowflake/cli/{plugins → _plugins}/cortex/commands.py +54 -49
  23. snowflake/cli/{plugins → _plugins}/cortex/constants.py +1 -1
  24. snowflake/cli/{plugins → _plugins}/cortex/manager.py +5 -5
  25. snowflake/cli/{plugins → _plugins}/cortex/plugin_spec.py +1 -1
  26. snowflake/cli/{plugins → _plugins}/git/commands.py +11 -7
  27. snowflake/cli/{plugins → _plugins}/git/manager.py +55 -9
  28. snowflake/cli/{plugins → _plugins}/git/plugin_spec.py +1 -1
  29. snowflake/cli/_plugins/helpers/commands.py +90 -0
  30. snowflake/cli/{plugins/notebook → _plugins/helpers}/plugin_spec.py +1 -1
  31. snowflake/cli/{plugins → _plugins}/init/commands.py +2 -2
  32. snowflake/cli/{plugins → _plugins}/init/plugin_spec.py +1 -1
  33. snowflake/cli/{plugins → _plugins}/nativeapp/artifacts.py +24 -9
  34. snowflake/cli/_plugins/nativeapp/bundle_context.py +31 -0
  35. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/artifact_processor.py +4 -4
  36. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/compiler.py +37 -18
  37. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +249 -0
  38. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  39. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/extension_function_utils.py +5 -5
  40. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/models.py +1 -1
  41. snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/python_processor.py +29 -34
  42. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +114 -0
  43. snowflake/cli/{plugins → _plugins}/nativeapp/commands.py +252 -132
  44. snowflake/cli/{plugins → _plugins}/nativeapp/common_flags.py +1 -1
  45. snowflake/cli/_plugins/nativeapp/entities/application.py +878 -0
  46. snowflake/cli/_plugins/nativeapp/entities/application_package.py +1392 -0
  47. snowflake/cli/{plugins → _plugins}/nativeapp/exceptions.py +3 -12
  48. snowflake/cli/_plugins/nativeapp/manager.py +415 -0
  49. snowflake/cli/{plugins/connection → _plugins/nativeapp}/plugin_spec.py +1 -1
  50. snowflake/cli/{plugins → _plugins}/nativeapp/policy.py +3 -0
  51. snowflake/cli/{plugins → _plugins}/nativeapp/project_model.py +36 -20
  52. snowflake/cli/_plugins/nativeapp/run_processor.py +184 -0
  53. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +70 -0
  54. snowflake/cli/_plugins/nativeapp/teardown_processor.py +70 -0
  55. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +262 -0
  56. snowflake/cli/{plugins → _plugins}/nativeapp/version/commands.py +20 -49
  57. snowflake/cli/_plugins/nativeapp/version/version_processor.py +98 -0
  58. snowflake/cli/{plugins → _plugins}/notebook/commands.py +3 -2
  59. snowflake/cli/{plugins → _plugins}/notebook/manager.py +5 -5
  60. snowflake/cli/{plugins/nativeapp → _plugins/notebook}/plugin_spec.py +1 -1
  61. snowflake/cli/{plugins → _plugins}/object/command_aliases.py +4 -4
  62. snowflake/cli/{plugins → _plugins}/object/commands.py +4 -5
  63. snowflake/cli/{plugins → _plugins}/object/manager.py +36 -15
  64. snowflake/cli/{plugins → _plugins}/object/plugin_spec.py +1 -1
  65. snowflake/cli/_plugins/snowpark/commands.py +450 -0
  66. snowflake/cli/_plugins/snowpark/common.py +268 -0
  67. snowflake/cli/{plugins → _plugins}/snowpark/models.py +0 -7
  68. snowflake/cli/{plugins → _plugins}/snowpark/package/anaconda_packages.py +2 -36
  69. snowflake/cli/{plugins → _plugins}/snowpark/package/commands.py +13 -74
  70. snowflake/cli/{plugins → _plugins}/snowpark/package/manager.py +2 -2
  71. snowflake/cli/{plugins → _plugins}/snowpark/package_utils.py +5 -5
  72. snowflake/cli/_plugins/snowpark/plugin_spec.py +30 -0
  73. snowflake/cli/_plugins/snowpark/snowpark_entity.py +29 -0
  74. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +173 -0
  75. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +109 -0
  76. snowflake/cli/{plugins → _plugins}/snowpark/snowpark_shared.py +0 -36
  77. snowflake/cli/{plugins → _plugins}/snowpark/zipper.py +16 -8
  78. snowflake/cli/{plugins → _plugins}/spcs/__init__.py +5 -7
  79. snowflake/cli/{plugins → _plugins}/spcs/compute_pool/commands.py +8 -8
  80. snowflake/cli/{plugins → _plugins}/spcs/compute_pool/manager.py +3 -3
  81. snowflake/cli/{plugins → _plugins}/spcs/image_registry/commands.py +3 -3
  82. snowflake/cli/{plugins → _plugins}/spcs/image_repository/commands.py +6 -6
  83. snowflake/cli/{plugins → _plugins}/spcs/image_repository/manager.py +1 -1
  84. snowflake/cli/{plugins → _plugins}/spcs/plugin_spec.py +1 -1
  85. snowflake/cli/{plugins → _plugins}/spcs/services/commands.py +44 -11
  86. snowflake/cli/{plugins → _plugins}/spcs/services/manager.py +43 -5
  87. snowflake/cli/{plugins → _plugins}/sql/commands.py +20 -17
  88. snowflake/cli/{plugins → _plugins}/sql/manager.py +1 -1
  89. snowflake/cli/{plugins → _plugins}/sql/plugin_spec.py +1 -1
  90. snowflake/cli/{plugins → _plugins}/stage/commands.py +15 -14
  91. snowflake/cli/{plugins → _plugins}/stage/diff.py +1 -47
  92. snowflake/cli/{plugins → _plugins}/stage/manager.py +12 -7
  93. snowflake/cli/{plugins → _plugins}/stage/plugin_spec.py +1 -1
  94. snowflake/cli/_plugins/stage/utils.py +54 -0
  95. snowflake/cli/{plugins → _plugins}/streamlit/commands.py +64 -48
  96. snowflake/cli/{plugins → _plugins}/streamlit/manager.py +67 -69
  97. snowflake/cli/_plugins/streamlit/plugin_spec.py +30 -0
  98. snowflake/cli/_plugins/streamlit/streamlit_entity.py +12 -0
  99. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +66 -0
  100. snowflake/cli/_plugins/workspace/action_context.py +18 -0
  101. snowflake/cli/_plugins/workspace/commands.py +306 -0
  102. snowflake/cli/_plugins/workspace/manager.py +74 -0
  103. snowflake/cli/_plugins/workspace/plugin_spec.py +30 -0
  104. snowflake/cli/api/cli_global_context.py +152 -295
  105. snowflake/cli/api/commands/common.py +25 -0
  106. snowflake/cli/api/commands/decorators.py +19 -4
  107. snowflake/cli/api/commands/experimental_behaviour.py +2 -3
  108. snowflake/cli/api/commands/flags.py +127 -228
  109. snowflake/cli/api/commands/overrideable_parameter.py +143 -0
  110. snowflake/cli/api/commands/snow_typer.py +21 -11
  111. snowflake/cli/api/commands/utils.py +18 -0
  112. snowflake/cli/api/config.py +44 -12
  113. snowflake/cli/api/connections.py +216 -0
  114. snowflake/cli/api/console/abc.py +8 -3
  115. snowflake/cli/api/constants.py +11 -0
  116. snowflake/cli/api/entities/common.py +56 -0
  117. snowflake/cli/api/entities/utils.py +370 -0
  118. snowflake/cli/api/errno.py +1 -0
  119. snowflake/cli/api/exceptions.py +31 -5
  120. snowflake/cli/api/feature_flags.py +0 -1
  121. snowflake/cli/api/identifiers.py +28 -5
  122. snowflake/cli/api/metrics.py +92 -0
  123. snowflake/cli/api/project/definition.py +48 -6
  124. snowflake/cli/api/project/definition_conversion.py +400 -0
  125. snowflake/cli/api/project/definition_manager.py +16 -5
  126. snowflake/cli/api/project/project_verification.py +3 -3
  127. snowflake/cli/api/project/schemas/entities/common.py +91 -16
  128. snowflake/cli/api/project/schemas/entities/entities.py +37 -6
  129. snowflake/cli/api/project/schemas/project_definition.py +180 -49
  130. snowflake/cli/api/project/schemas/updatable_model.py +11 -3
  131. snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
  132. snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +3 -1
  133. snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
  134. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +8 -9
  135. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
  136. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +7 -1
  137. snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
  138. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
  139. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
  140. snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
  141. snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
  142. snowflake/cli/api/project/util.py +23 -6
  143. snowflake/cli/api/rendering/jinja.py +14 -8
  144. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  145. snowflake/cli/api/rendering/sql_templates.py +56 -11
  146. snowflake/cli/api/rest_api.py +11 -5
  147. snowflake/cli/api/secure_path.py +16 -18
  148. snowflake/cli/api/secure_utils.py +90 -1
  149. snowflake/cli/api/sql_execution.py +43 -23
  150. snowflake/cli/api/utils/definition_rendering.py +45 -13
  151. {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.1.dist-info}/METADATA +18 -18
  152. snowflake_cli-3.0.1.dist-info/RECORD +242 -0
  153. snowflake_cli-3.0.1.dist-info/entry_points.txt +2 -0
  154. snowflake/cli/api/commands/project_initialisation.py +0 -65
  155. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  156. snowflake/cli/api/project/schemas/entities/application_entity.py +0 -44
  157. snowflake/cli/api/project/schemas/entities/application_package_entity.py +0 -66
  158. snowflake/cli/app/build_and_push.sh +0 -8
  159. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +0 -172
  160. snowflake/cli/plugins/nativeapp/init.py +0 -345
  161. snowflake/cli/plugins/nativeapp/manager.py +0 -823
  162. snowflake/cli/plugins/nativeapp/run_processor.py +0 -389
  163. snowflake/cli/plugins/nativeapp/teardown_processor.py +0 -301
  164. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +0 -135
  165. snowflake/cli/plugins/nativeapp/version/version_processor.py +0 -362
  166. snowflake/cli/plugins/object_stage_deprecated/__init__.py +0 -15
  167. snowflake/cli/plugins/object_stage_deprecated/commands.py +0 -122
  168. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +0 -32
  169. snowflake/cli/plugins/snowpark/commands.py +0 -546
  170. snowflake/cli/plugins/snowpark/common.py +0 -307
  171. snowflake/cli/plugins/snowpark/manager.py +0 -109
  172. snowflake/cli/plugins/snowpark/plugin_spec.py +0 -30
  173. snowflake/cli/plugins/snowpark/snowpark_package_paths.py +0 -65
  174. snowflake/cli/plugins/spcs/jobs/commands.py +0 -78
  175. snowflake/cli/plugins/spcs/jobs/manager.py +0 -53
  176. snowflake/cli/plugins/streamlit/__init__.py +0 -13
  177. snowflake/cli/plugins/streamlit/plugin_spec.py +0 -30
  178. snowflake/cli/plugins/workspace/__init__.py +0 -13
  179. snowflake/cli/plugins/workspace/commands.py +0 -35
  180. snowflake/cli/plugins/workspace/plugin_spec.py +0 -30
  181. snowflake/cli/templates/default_snowpark/.gitignore +0 -4
  182. snowflake/cli/templates/default_snowpark/app/common.py +0 -2
  183. snowflake/cli/templates/default_snowpark/app/functions.py +0 -15
  184. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -22
  185. snowflake/cli/templates/default_snowpark/requirements.txt +0 -1
  186. snowflake/cli/templates/default_snowpark/snowflake.yml +0 -23
  187. snowflake/cli/templates/default_streamlit/.gitignore +0 -4
  188. snowflake/cli/templates/default_streamlit/common/hello.py +0 -2
  189. snowflake/cli/templates/default_streamlit/environment.yml +0 -6
  190. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -3
  191. snowflake/cli/templates/default_streamlit/snowflake.yml +0 -10
  192. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -4
  193. snowflake_cli-2.8.2.dist-info/RECORD +0 -240
  194. snowflake_cli-2.8.2.dist-info/entry_points.txt +0 -2
  195. /snowflake/cli/{app → _app}/__init__.py +0 -0
  196. /snowflake/cli/{api/project/schemas/native_app → _app/api_impl}/__init__.py +0 -0
  197. /snowflake/cli/{api/project/schemas/snowpark → _app/api_impl/plugin}/__init__.py +0 -0
  198. /snowflake/cli/{app → _app}/api_impl/plugin/plugin_config_provider_impl.py +0 -0
  199. /snowflake/cli/{app → _app}/commands_registration/__init__.py +0 -0
  200. /snowflake/cli/{app → _app}/commands_registration/threadsafe.py +0 -0
  201. /snowflake/cli/{app → _app}/constants.py +0 -0
  202. /snowflake/cli/{api/project/schemas/streamlit → _app/dev}/__init__.py +0 -0
  203. /snowflake/cli/{app → _app}/dev/commands_structure.py +0 -0
  204. /snowflake/cli/{app/api_impl → _app/dev/docs}/__init__.py +0 -0
  205. /snowflake/cli/{app → _app}/dev/docs/project_definition_generate_json_schema.py +0 -0
  206. /snowflake/cli/{app → _app}/dev/docs/template_utils.py +0 -0
  207. /snowflake/cli/{app → _app}/dev/docs/templates/definition_description.rst.jinja2 +0 -0
  208. /snowflake/cli/{app → _app}/dev/docs/templates/overview.rst.jinja2 +0 -0
  209. /snowflake/cli/{app → _app}/dev/pycharm_remote_debug.py +0 -0
  210. /snowflake/cli/{app → _app}/loggers.py +0 -0
  211. /snowflake/cli/{app/api_impl/plugin → _plugins}/__init__.py +0 -0
  212. /snowflake/cli/{app/dev → _plugins/connection}/__init__.py +0 -0
  213. /snowflake/cli/{app/dev/docs → _plugins/cortex}/__init__.py +0 -0
  214. /snowflake/cli/{plugins → _plugins}/cortex/types.py +0 -0
  215. /snowflake/cli/{plugins → _plugins/git}/__init__.py +0 -0
  216. /snowflake/cli/{plugins/connection → _plugins/helpers}/__init__.py +0 -0
  217. /snowflake/cli/{plugins/cortex → _plugins/init}/__init__.py +0 -0
  218. /snowflake/cli/{plugins/git → _plugins/nativeapp}/__init__.py +0 -0
  219. /snowflake/cli/{plugins/init → _plugins/nativeapp/codegen}/__init__.py +0 -0
  220. /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/sandbox.py +0 -0
  221. /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/callback_source.py.jinja +0 -0
  222. /snowflake/cli/{plugins → _plugins}/nativeapp/constants.py +0 -0
  223. /snowflake/cli/{templates/default_snowpark/app → _plugins/nativeapp/entities}/__init__.py +0 -0
  224. /snowflake/cli/{plugins → _plugins}/nativeapp/feature_flags.py +0 -0
  225. /snowflake/cli/{plugins → _plugins}/nativeapp/utils.py +0 -0
  226. /snowflake/cli/{plugins/nativeapp → _plugins/nativeapp/version}/__init__.py +0 -0
  227. /snowflake/cli/{plugins/nativeapp/codegen → _plugins/notebook}/__init__.py +0 -0
  228. /snowflake/cli/{plugins → _plugins}/notebook/exceptions.py +0 -0
  229. /snowflake/cli/{plugins → _plugins}/notebook/types.py +0 -0
  230. /snowflake/cli/{plugins/nativeapp/version → _plugins/object}/__init__.py +0 -0
  231. /snowflake/cli/{plugins → _plugins}/object/common.py +0 -0
  232. /snowflake/cli/{plugins/notebook → _plugins/snowpark}/__init__.py +0 -0
  233. /snowflake/cli/{plugins/object → _plugins/snowpark/package}/__init__.py +0 -0
  234. /snowflake/cli/{plugins → _plugins}/snowpark/package/utils.py +0 -0
  235. /snowflake/cli/{plugins → _plugins}/spcs/common.py +0 -0
  236. /snowflake/cli/{plugins/snowpark → _plugins/spcs/compute_pool}/__init__.py +0 -0
  237. /snowflake/cli/{plugins/snowpark/package → _plugins/spcs/image_registry}/__init__.py +0 -0
  238. /snowflake/cli/{plugins → _plugins}/spcs/image_registry/manager.py +0 -0
  239. /snowflake/cli/{plugins/spcs/compute_pool → _plugins/spcs/image_repository}/__init__.py +0 -0
  240. /snowflake/cli/{plugins/spcs/image_registry → _plugins/spcs/services}/__init__.py +0 -0
  241. /snowflake/cli/{plugins/spcs/image_repository → _plugins/sql}/__init__.py +0 -0
  242. /snowflake/cli/{plugins → _plugins}/sql/snowsql_templating.py +0 -0
  243. /snowflake/cli/{plugins/spcs/jobs → _plugins/stage}/__init__.py +0 -0
  244. /snowflake/cli/{plugins → _plugins}/stage/md5.py +0 -0
  245. /snowflake/cli/{plugins/spcs/services → _plugins/streamlit}/__init__.py +0 -0
  246. /snowflake/cli/{plugins/sql → _plugins/workspace}/__init__.py +0 -0
  247. /snowflake/cli/{plugins/stage → api/project/schemas/entities}/__init__.py +0 -0
  248. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
  249. /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
  250. {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.1.dist-info}/WHEEL +0 -0
  251. {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -34,6 +34,9 @@ UNQUOTED_IDENTIFIER_REGEX = r"([a-zA-Z_])([a-zA-Z0-9_$]{0,254})"
34
34
  QUOTED_IDENTIFIER_REGEX = r'"((""|[^"]){0,255})"'
35
35
  VALID_IDENTIFIER_REGEX = f"(?:{UNQUOTED_IDENTIFIER_REGEX}|{QUOTED_IDENTIFIER_REGEX})"
36
36
 
37
+ # An env var that is used to suffix the names of some account-level resources
38
+ TEST_RESOURCE_SUFFIX_VAR = "SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX"
39
+
37
40
 
38
41
  def encode_uri_component(s: str) -> str:
39
42
  """
@@ -191,12 +194,6 @@ def extract_schema(qualified_name: str):
191
194
  return None
192
195
 
193
196
 
194
- def generate_user_env(username: str) -> dict:
195
- return {
196
- "USER": username,
197
- }
198
-
199
-
200
197
  def first_set_env(*keys: str):
201
198
  for k in keys:
202
199
  v = os.getenv(k)
@@ -259,3 +256,23 @@ def identifier_to_show_like_pattern(identifier: str) -> str:
259
256
  matching this identifier
260
257
  """
261
258
  return f"'{escape_like_pattern(unquote_identifier(identifier))}'"
259
+
260
+
261
+ def append_test_resource_suffix(identifier: str) -> str:
262
+ """
263
+ Append a suffix that should be added to specified account-level resources.
264
+
265
+ This is an internal concern that is currently only used in tests
266
+ to isolate concurrent runs and to add the test name to resources.
267
+ """
268
+ suffix = os.environ.get(TEST_RESOURCE_SUFFIX_VAR, "")
269
+ if identifier_to_str(identifier).endswith(identifier_to_str(suffix)):
270
+ # If the suffix has already been added, don't add it again
271
+ return identifier
272
+ if is_valid_quoted_identifier(identifier) or is_valid_quoted_identifier(suffix):
273
+ # If either identifier is already quoted, use concat_identifier
274
+ # to add the suffix inside the quotes
275
+ return concat_identifiers([identifier, suffix])
276
+ # Otherwise just append the string, don't add quotes
277
+ # in case the user doesn't want them
278
+ return f"{identifier}{suffix}"
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  from pathlib import Path
19
19
  from textwrap import dedent
20
- from typing import Dict, Optional
20
+ from typing import Any, Dict, Optional
21
21
 
22
22
  import jinja2
23
23
  from jinja2 import Environment, StrictUndefined, loaders
@@ -82,8 +82,18 @@ class IgnoreAttrEnvironment(Environment):
82
82
  return self.undefined(obj=obj, name=argument)
83
83
 
84
84
 
85
+ def get_basic_jinja_env(loader: Optional[loaders.BaseLoader] = None) -> Environment:
86
+ return env_bootstrap(
87
+ IgnoreAttrEnvironment(
88
+ loader=loader or loaders.BaseLoader(),
89
+ keep_trailing_newline=True,
90
+ undefined=StrictUndefined,
91
+ )
92
+ )
93
+
94
+
85
95
  def jinja_render_from_file(
86
- template_path: Path, data: Dict, output_file_path: Optional[Path] = None
96
+ template_path: Path, data: Dict[str, Any], output_file_path: Optional[Path] = None
87
97
  ) -> Optional[str]:
88
98
  """
89
99
  Renders a jinja template and outputs either the rendered contents as string or writes to a file.
@@ -96,12 +106,8 @@ def jinja_render_from_file(
96
106
  Returns:
97
107
  None if file path is provided, else returns the rendered string.
98
108
  """
99
- env = env_bootstrap(
100
- IgnoreAttrEnvironment(
101
- loader=loaders.FileSystemLoader(template_path.parent),
102
- keep_trailing_newline=True,
103
- undefined=StrictUndefined,
104
- )
109
+ env = get_basic_jinja_env(
110
+ loader=loaders.FileSystemLoader(template_path.parent.as_posix())
105
111
  )
106
112
  loaded_template = env.get_template(template_path.name)
107
113
  rendered_result = loaded_template.render(**data)
@@ -24,7 +24,11 @@ _YML_TEMPLATE_START = "<%"
24
24
  _YML_TEMPLATE_END = "%>"
25
25
 
26
26
 
27
- def get_project_definition_cli_jinja_env() -> Environment:
27
+ def has_client_side_templates(template_content: str) -> bool:
28
+ return _YML_TEMPLATE_START in template_content
29
+
30
+
31
+ def get_client_side_jinja_env() -> Environment:
28
32
  _random_block = "___very___unique___block___to___disable___logic___blocks___"
29
33
  return env_bootstrap(
30
34
  IgnoreAttrEnvironment(
@@ -17,8 +17,11 @@ from __future__ import annotations
17
17
  from typing import Dict, Optional
18
18
 
19
19
  from click import ClickException
20
- from jinja2 import StrictUndefined, loaders
21
- from snowflake.cli.api.cli_global_context import cli_context
20
+ from jinja2 import Environment, StrictUndefined, loaders, meta
21
+ from snowflake.cli.api.cli_global_context import get_cli_context
22
+ from snowflake.cli.api.console.console import cli_console
23
+ from snowflake.cli.api.exceptions import InvalidTemplate
24
+ from snowflake.cli.api.metrics import CLICounterField
22
25
  from snowflake.cli.api.rendering.jinja import (
23
26
  CONTEXT_KEY,
24
27
  FUNCTION_KEY,
@@ -26,26 +29,62 @@ from snowflake.cli.api.rendering.jinja import (
26
29
  env_bootstrap,
27
30
  )
28
31
 
29
- _SQL_TEMPLATE_START = "&{"
30
- _SQL_TEMPLATE_END = "}"
32
+ _SQL_TEMPLATE_START = "<%"
33
+ _SQL_TEMPLATE_END = "%>"
34
+ _OLD_SQL_TEMPLATE_START = "&{"
35
+ _OLD_SQL_TEMPLATE_END = "}"
31
36
  RESERVED_KEYS = [CONTEXT_KEY, FUNCTION_KEY]
32
37
 
33
38
 
34
- def get_sql_cli_jinja_env(*, loader: Optional[loaders.BaseLoader] = None):
39
+ def _get_sql_jinja_env(template_start: str, template_end: str) -> Environment:
35
40
  _random_block = "___very___unique___block___to___disable___logic___blocks___"
36
41
  return env_bootstrap(
37
42
  IgnoreAttrEnvironment(
38
- loader=loader or loaders.BaseLoader(),
39
- keep_trailing_newline=True,
40
- variable_start_string=_SQL_TEMPLATE_START,
41
- variable_end_string=_SQL_TEMPLATE_END,
43
+ variable_start_string=template_start,
44
+ variable_end_string=template_end,
45
+ loader=loaders.BaseLoader(),
42
46
  block_start_string=_random_block,
43
47
  block_end_string=_random_block,
48
+ keep_trailing_newline=True,
44
49
  undefined=StrictUndefined,
45
50
  )
46
51
  )
47
52
 
48
53
 
54
+ def _does_template_have_env_syntax(env: Environment, template_content: str) -> bool:
55
+ template = env.parse(template_content)
56
+ return bool(meta.find_undeclared_variables(template))
57
+
58
+
59
+ def has_sql_templates(template_content: str) -> bool:
60
+ return (
61
+ _OLD_SQL_TEMPLATE_START in template_content
62
+ or _SQL_TEMPLATE_START in template_content
63
+ )
64
+
65
+
66
+ def choose_sql_jinja_env_based_on_template_syntax(
67
+ template_content: str, reference_name: Optional[str] = None
68
+ ) -> Environment:
69
+ old_syntax_env = _get_sql_jinja_env(_OLD_SQL_TEMPLATE_START, _OLD_SQL_TEMPLATE_END)
70
+ new_syntax_env = _get_sql_jinja_env(_SQL_TEMPLATE_START, _SQL_TEMPLATE_END)
71
+ has_old_syntax = _does_template_have_env_syntax(old_syntax_env, template_content)
72
+ has_new_syntax = _does_template_have_env_syntax(new_syntax_env, template_content)
73
+ reference_name_str = f" in {reference_name}" if reference_name else ""
74
+ if has_old_syntax and has_new_syntax:
75
+ raise InvalidTemplate(
76
+ f"The SQL query{reference_name_str} mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
77
+ f" and {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax."
78
+ )
79
+ if has_old_syntax:
80
+ cli_console.warning(
81
+ f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax{reference_name_str} is deprecated."
82
+ f" Use {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax instead."
83
+ )
84
+ return old_syntax_env
85
+ return new_syntax_env
86
+
87
+
49
88
  def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
50
89
  data = data or {}
51
90
 
@@ -55,6 +94,12 @@ def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
55
94
  f"{reserved_key} in user defined data. The `{reserved_key}` variable is reserved for CLI usage."
56
95
  )
57
96
 
58
- context_data = cli_context.template_context
97
+ context_data = get_cli_context().template_context
59
98
  context_data.update(data)
60
- return get_sql_cli_jinja_env().from_string(content).render(**context_data)
99
+ env = choose_sql_jinja_env_based_on_template_syntax(content)
100
+
101
+ get_cli_context().metrics.set_counter(
102
+ CLICounterField.SQL_TEMPLATES, int(has_sql_templates(content))
103
+ )
104
+
105
+ return env.from_string(content).render(context_data)
@@ -21,8 +21,9 @@ from typing import Any, Dict, Optional
21
21
  from click import ClickException
22
22
  from snowflake.cli.api.constants import SF_REST_API_URL_PREFIX
23
23
  from snowflake.connector.connection import SnowflakeConnection
24
- from snowflake.connector.errors import BadRequest, InterfaceError
24
+ from snowflake.connector.errors import BadRequest
25
25
  from snowflake.connector.network import SnowflakeRestful
26
+ from snowflake.connector.vendored.requests.exceptions import HTTPError
26
27
 
27
28
  log = logging.getLogger(__name__)
28
29
 
@@ -47,10 +48,10 @@ class RestApi:
47
48
  Check whether [get] endpoint exists under given URL.
48
49
  """
49
50
  try:
50
- result = self.send_rest_request(url, method="get")
51
- return bool(result) or result == []
52
- except InterfaceError as err:
53
- if "404 Not Found" in str(err):
51
+ self.send_rest_request(url, method="get")
52
+ return True
53
+ except HTTPError as err:
54
+ if err.response.status_code == 404:
54
55
  return False
55
56
  raise err
56
57
 
@@ -60,6 +61,10 @@ class RestApi:
60
61
  return bool(result)
61
62
  except BadRequest:
62
63
  return False
64
+ except HTTPError as err:
65
+ if err.response.status_code == 404:
66
+ return False
67
+ raise err
63
68
 
64
69
  def send_rest_request(
65
70
  self, url: str, method: str, data: Optional[Dict[str, Any]] = None
@@ -91,6 +96,7 @@ class RestApi:
91
96
  token=self.rest.token,
92
97
  data=json.dumps(data if data else {}),
93
98
  no_retry=True,
99
+ raise_raw_http_failure=True,
94
100
  )
95
101
 
96
102
  def _database_exists(self, db_name: str) -> bool:
@@ -24,6 +24,12 @@ from pathlib import Path
24
24
  from typing import Optional, Union
25
25
 
26
26
  from snowflake.cli.api.exceptions import DirectoryIsNotEmptyError, FileTooLargeError
27
+ from snowflake.cli.api.secure_utils import (
28
+ chmod as secure_chmod,
29
+ )
30
+ from snowflake.cli.api.secure_utils import (
31
+ restrict_file_permissions,
32
+ )
27
33
 
28
34
  log = logging.getLogger(__name__)
29
35
 
@@ -47,6 +53,12 @@ class SecurePath:
47
53
  """
48
54
  return self._path
49
55
 
56
+ def chmod(self, permissions_mask: int) -> None:
57
+ """
58
+ Change the file mode and permissions, like os.chmod().
59
+ """
60
+ secure_chmod(self._path, permissions_mask)
61
+
50
62
  @property
51
63
  def parent(self):
52
64
  """
@@ -97,28 +109,11 @@ class SecurePath:
97
109
  """A string representing the final path component."""
98
110
  return self._path.name
99
111
 
100
- def chmod(self, permissions_mask: int) -> None:
101
- """
102
- Change the file mode and permissions, like os.chmod().
103
- """
104
- log.info(
105
- "Update permissions of file %s to %s", self._path, oct(permissions_mask)
106
- )
107
- self._path.chmod(permissions_mask)
108
-
109
112
  def restrict_permissions(self) -> None:
110
113
  """
111
114
  Restrict file/directory permissions to owner-only.
112
115
  """
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)
116
+ restrict_file_permissions(self._path)
122
117
 
123
118
  def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
124
119
  """
@@ -349,6 +344,9 @@ class SecurePath:
349
344
  ):
350
345
  raise FileTooLargeError(self._path.resolve(), size_limit_in_mb)
351
346
 
347
+ def rename(self, new_name: Union[str | Path]):
348
+ self._path.rename(new_name)
349
+
352
350
 
353
351
  def _raise_file_exists_error(path: Path):
354
352
  raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), path)
@@ -12,11 +12,64 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import logging
15
16
  import stat
16
17
  from pathlib import Path
18
+ from typing import List
17
19
 
20
+ from snowflake.connector.compat import IS_WINDOWS
18
21
 
19
- def file_permissions_are_strict(file_path: Path) -> bool:
22
+ log = logging.getLogger(__name__)
23
+
24
+
25
+ def _get_windows_whitelisted_users():
26
+ # whitelisted users list obtained in consultation with prodsec: CASEC-9627
27
+ import os
28
+
29
+ return [
30
+ "SYSTEM",
31
+ "Administrators",
32
+ "Network",
33
+ "Domain Admins",
34
+ "Domain Users",
35
+ os.getlogin(),
36
+ ]
37
+
38
+
39
+ def _run_icacls(file_path: Path) -> str:
40
+ import subprocess
41
+
42
+ return subprocess.check_output(["icacls", str(file_path)], text=True)
43
+
44
+
45
+ def _windows_permissions_are_denied(permission_codes: str) -> bool:
46
+ # according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
47
+ return "(DENY)" in permission_codes or "(N)" in permission_codes
48
+
49
+
50
+ def windows_get_not_whitelisted_users_with_access(file_path: Path) -> List[str]:
51
+ import re
52
+
53
+ # according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
54
+ icacls_output_regex = (
55
+ rf"({re.escape(str(file_path))})?.*\\(?P<user>.*):(?P<permissions>[(A-Z),]+)"
56
+ )
57
+ whitelisted_users = _get_windows_whitelisted_users()
58
+
59
+ users_with_access = []
60
+ for permission in re.finditer(icacls_output_regex, _run_icacls(file_path)):
61
+ if (permission.group("user") not in whitelisted_users) and (
62
+ not _windows_permissions_are_denied(permission.group("permissions"))
63
+ ):
64
+ users_with_access.append(permission.group("user"))
65
+ return list(set(users_with_access))
66
+
67
+
68
+ def _windows_file_permissions_are_strict(file_path: Path) -> bool:
69
+ return windows_get_not_whitelisted_users_with_access(file_path) == []
70
+
71
+
72
+ def _unix_file_permissions_are_strict(file_path: Path) -> bool:
20
73
  accessible_by_others = (
21
74
  # https://docs.python.org/3/library/stat.html
22
75
  stat.S_IRGRP # readable by group
@@ -27,3 +80,39 @@ def file_permissions_are_strict(file_path: Path) -> bool:
27
80
  | stat.S_IXOTH # executable by others
28
81
  )
29
82
  return (file_path.stat().st_mode & accessible_by_others) == 0
83
+
84
+
85
+ def file_permissions_are_strict(file_path: Path) -> bool:
86
+ if IS_WINDOWS:
87
+ return _windows_file_permissions_are_strict(file_path)
88
+ return _unix_file_permissions_are_strict(file_path)
89
+
90
+
91
+ def chmod(path: Path, permissions_mask: int) -> None:
92
+ log.info("Update permissions of file %s to %s", path, oct(permissions_mask))
93
+ path.chmod(permissions_mask)
94
+
95
+
96
+ def _unix_restrict_file_permissions(path: Path) -> None:
97
+ owner_permissions = (
98
+ # https://docs.python.org/3/library/stat.html
99
+ stat.S_IRUSR # readable by owner
100
+ | stat.S_IWUSR # writeable by owner
101
+ | stat.S_IXUSR # executable by owner
102
+ )
103
+ chmod(path, path.stat().st_mode & owner_permissions)
104
+
105
+
106
+ def _windows_restrict_file_permissions(path: Path) -> None:
107
+ import subprocess
108
+
109
+ for user in windows_get_not_whitelisted_users_with_access(path):
110
+ log.info("Removing permissions of user %s from file %s", user, path)
111
+ subprocess.run(["icacls", str(path), "/DENY", f"{user}:F"])
112
+
113
+
114
+ def restrict_file_permissions(file_path: Path) -> None:
115
+ if IS_WINDOWS:
116
+ _windows_restrict_file_permissions(file_path)
117
+ else:
118
+ _unix_restrict_file_permissions(file_path)
@@ -21,7 +21,7 @@ from io import StringIO
21
21
  from textwrap import dedent
22
22
  from typing import Iterable, Optional, Tuple
23
23
 
24
- from snowflake.cli.api.cli_global_context import cli_context
24
+ from snowflake.cli.api.cli_global_context import get_cli_context
25
25
  from snowflake.cli.api.console import cli_console
26
26
  from snowflake.cli.api.constants import ObjectType
27
27
  from snowflake.cli.api.exceptions import (
@@ -35,27 +35,21 @@ from snowflake.cli.api.project.util import (
35
35
  unquote_identifier,
36
36
  )
37
37
  from snowflake.cli.api.utils.cursor import find_first_row
38
+ from snowflake.connector import SnowflakeConnection
38
39
  from snowflake.connector.cursor import DictCursor, SnowflakeCursor
39
40
  from snowflake.connector.errors import ProgrammingError
40
41
 
41
42
 
42
- class SqlExecutionMixin:
43
- def __init__(self):
43
+ class SqlExecutor:
44
+ def __init__(self, connection: SnowflakeConnection | None = None):
44
45
  self._snowpark_session = None
46
+ self._connection = connection
45
47
 
46
48
  @property
47
- def _conn(self):
48
- return cli_context.connection
49
-
50
- @property
51
- def snowpark_session(self):
52
- if not self._snowpark_session:
53
- from snowflake.snowpark.session import Session
54
-
55
- self._snowpark_session = Session.builder.configs(
56
- {"connection": self._conn}
57
- ).create()
58
- return self._snowpark_session
49
+ def _conn(self) -> SnowflakeConnection:
50
+ if self._connection:
51
+ return self._connection
52
+ return get_cli_context().connection
59
53
 
60
54
  @cached_property
61
55
  def _log(self):
@@ -88,6 +82,12 @@ class SqlExecutionMixin:
88
82
  def _execute_queries(self, queries: str, **kwargs):
89
83
  return list(self._execute_string(dedent(queries), **kwargs))
90
84
 
85
+ def execute_query(self, query: str, **kwargs):
86
+ return self._execute_query(query, **kwargs)
87
+
88
+ def execute_queries(self, queries: str, **kwargs):
89
+ return self._execute_queries(queries, **kwargs)
90
+
91
91
  def use(self, object_type: ObjectType, name: str):
92
92
  try:
93
93
  self._execute_query(f"use {object_type.value.sf_name} {name}")
@@ -97,16 +97,16 @@ class SqlExecutionMixin:
97
97
  f"Could not use {object_type} {name}. Object does not exist, or operation cannot be performed."
98
98
  )
99
99
 
100
+ def current_role(self) -> str:
101
+ return self._execute_query(f"select current_role()").fetchone()[0]
102
+
100
103
  @contextmanager
101
104
  def use_role(self, new_role: str):
102
105
  """
103
106
  Switches to a different role for a while, then switches back.
104
107
  This is a no-op if the requested role is already active.
105
108
  """
106
- role_result = self._execute_query(
107
- f"select current_role()", cursor_class=DictCursor
108
- ).fetchone()
109
- prev_role = role_result["CURRENT_ROLE()"]
109
+ prev_role = self.current_role()
110
110
  is_different_role = new_role.lower() != prev_role.lower()
111
111
  if is_different_role:
112
112
  self._log.debug("Assuming different role: %s", new_role)
@@ -117,6 +117,12 @@ class SqlExecutionMixin:
117
117
  if is_different_role:
118
118
  self._execute_query(f"use role {prev_role}")
119
119
 
120
+ def session_has_warehouse(self) -> bool:
121
+ result = self._execute_query(
122
+ "select current_warehouse() is not null"
123
+ ).fetchone()
124
+ return bool(result[0])
125
+
120
126
  @contextmanager
121
127
  def use_warehouse(self, new_wh: str):
122
128
  """
@@ -125,12 +131,10 @@ class SqlExecutionMixin:
125
131
  If there is no default warehouse in the account, it will throw an error.
126
132
  """
127
133
 
128
- wh_result = self._execute_query(
129
- f"select current_warehouse()", cursor_class=DictCursor
130
- ).fetchone()
134
+ wh_result = self._execute_query(f"select current_warehouse()").fetchone()
131
135
  # If user has an assigned default warehouse, prev_wh will contain a value even if the warehouse is suspended.
132
136
  try:
133
- prev_wh = wh_result["CURRENT_WAREHOUSE()"]
137
+ prev_wh = wh_result[0]
134
138
  except:
135
139
  prev_wh = None
136
140
 
@@ -254,6 +258,22 @@ class SqlExecutionMixin:
254
258
  return show_obj_row
255
259
 
256
260
 
261
+ class SqlExecutionMixin(SqlExecutor):
262
+ def __init__(self, *args, **kwargs):
263
+ super().__init__(*args, **kwargs)
264
+ self._snowpark_session = None
265
+
266
+ @property
267
+ def snowpark_session(self):
268
+ if not self._snowpark_session:
269
+ from snowflake.snowpark.session import Session
270
+
271
+ self._snowpark_session = Session.builder.configs(
272
+ {"connection": self._conn}
273
+ ).create()
274
+ return self._snowpark_session
275
+
276
+
257
277
  class VerboseCursor(SnowflakeCursor):
258
278
  def execute(self, command: str, *args, **kwargs):
259
279
  cli_console.message(command)
@@ -19,16 +19,18 @@ from typing import Any, Optional
19
19
 
20
20
  from jinja2 import Environment, TemplateSyntaxError, nodes
21
21
  from packaging.version import Version
22
+ from snowflake.cli.api.cli_global_context import get_cli_context
22
23
  from snowflake.cli.api.console import cli_console as cc
23
24
  from snowflake.cli.api.exceptions import CycleDetectedError, InvalidTemplate
25
+ from snowflake.cli.api.metrics import CLICounterField
24
26
  from snowflake.cli.api.project.schemas.project_definition import (
25
27
  ProjectProperties,
26
28
  build_project_definition,
27
29
  )
28
30
  from snowflake.cli.api.project.schemas.updatable_model import context
29
- from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
31
+ from snowflake.cli.api.rendering.jinja import CONTEXT_KEY, FUNCTION_KEY
30
32
  from snowflake.cli.api.rendering.project_definition_templates import (
31
- get_project_definition_cli_jinja_env,
33
+ get_client_side_jinja_env,
32
34
  )
33
35
  from snowflake.cli.api.utils.dict_utils import deep_merge_dicts, traverse
34
36
  from snowflake.cli.api.utils.graph import Graph, Node
@@ -96,7 +98,7 @@ class TemplatedEnvironment:
96
98
  )
97
99
  or current_attr_chain is not None
98
100
  ):
99
- raise InvalidTemplate(f"Unexpected templating syntax in {template_value}")
101
+ raise InvalidTemplate(f"Unexpected template syntax in {template_value}")
100
102
 
101
103
  for child_node in ast_node.iter_child_nodes():
102
104
  all_referenced_vars.update(
@@ -266,6 +268,12 @@ def _get_referenced_vars_in_definition(
266
268
  return referenced_vars
267
269
 
268
270
 
271
+ def _has_referenced_vars_in_definition(
272
+ template_env: TemplatedEnvironment, definition: Definition
273
+ ) -> bool:
274
+ return len(_get_referenced_vars_in_definition(template_env, definition)) > 0
275
+
276
+
269
277
  def _template_version_warning():
270
278
  cc.warning(
271
279
  "Ignoring template pattern in project definition file. "
@@ -277,7 +285,9 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
277
285
  with context({"skip_validation_on_templates": True}):
278
286
  # pass a flag to Pydantic to skip validation for templated scalars
279
287
  # populate the defaults
280
- project_definition = build_project_definition(**original_definition)
288
+ project_definition = build_project_definition(
289
+ **copy.deepcopy(original_definition)
290
+ )
281
291
 
282
292
  definition_with_defaults = project_definition.model_dump(
283
293
  exclude_none=True, warnings=False, by_alias=True
@@ -289,6 +299,17 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
289
299
  return definition_with_defaults
290
300
 
291
301
 
302
+ def _update_metrics(template_env: TemplatedEnvironment, definition: Definition):
303
+ metrics = get_cli_context().metrics
304
+
305
+ # render_definition_template is invoked multiple times both by the user
306
+ # and by us so we should make sure we don't overwrite a 1 with a 0 here
307
+ metrics.set_counter_default(CLICounterField.PDF_TEMPLATES, 0)
308
+
309
+ if _has_referenced_vars_in_definition(template_env, definition):
310
+ metrics.set_counter(CLICounterField.PDF_TEMPLATES, 1)
311
+
312
+
292
313
  def render_definition_template(
293
314
  original_definition: Optional[Definition], context_overrides: Context
294
315
  ) -> ProjectProperties:
@@ -318,16 +339,13 @@ def render_definition_template(
318
339
  if definition is None:
319
340
  return ProjectProperties(None, {CONTEXT_KEY: {"env": environment_overrides}})
320
341
 
321
- template_env = TemplatedEnvironment(get_project_definition_cli_jinja_env())
342
+ template_env = TemplatedEnvironment(get_client_side_jinja_env())
322
343
 
323
344
  if "definition_version" not in definition or Version(
324
345
  definition["definition_version"]
325
346
  ) < Version("1.1"):
326
347
  try:
327
- referenced_vars = _get_referenced_vars_in_definition(
328
- template_env, definition
329
- )
330
- if referenced_vars:
348
+ if _has_referenced_vars_in_definition(template_env, definition):
331
349
  _template_version_warning()
332
350
  except Exception:
333
351
  # also warn on Exception, as it means the user is incorrectly attempting to use templating
@@ -338,13 +356,17 @@ def render_definition_template(
338
356
  project_context[CONTEXT_KEY]["env"] = environment_overrides
339
357
  return ProjectProperties(project_definition, project_context)
340
358
 
359
+ # need to have the metrics added here since we add defaults to the
360
+ # definition that the user might not have added themselves later
361
+ _update_metrics(template_env, definition)
362
+
341
363
  definition = _add_defaults_to_definition(definition)
342
364
  project_context = {CONTEXT_KEY: definition}
343
365
 
344
366
  _validate_env_section(definition.get("env", {}))
345
367
 
346
368
  # add available templating functions
347
- project_context["fn"] = get_templating_functions()
369
+ project_context[FUNCTION_KEY] = get_templating_functions()
348
370
 
349
371
  referenced_vars = _get_referenced_vars_in_definition(template_env, definition)
350
372
 
@@ -353,9 +375,7 @@ def render_definition_template(
353
375
  )
354
376
 
355
377
  def on_cycle_action(node: Node[TemplateVar]):
356
- raise CycleDetectedError(
357
- f"Cycle detected in templating variable {node.data.key}"
358
- )
378
+ raise CycleDetectedError(f"Cycle detected in template variable {node.data.key}")
359
379
 
360
380
  dependencies_graph.dfs(
361
381
  visit_action=lambda node: _render_graph_node(template_env, node),
@@ -374,10 +394,22 @@ def render_definition_template(
374
394
  )
375
395
 
376
396
  project_definition = build_project_definition(**definition)
397
+
398
+ # Use the values originally provided by the user as the template context
399
+ # This intentionally doesn't reflect any field changes made by
400
+ # validators, to minimize user surprise when templating values
377
401
  project_context[CONTEXT_KEY] = definition
402
+
378
403
  # Use `ProjectEnvironment` in project context in order to
379
404
  # handle env variables overrides from OS env and from CLI arguments.
380
405
  project_context[CONTEXT_KEY]["env"] = ProjectEnvironment(
381
406
  default_env=project_context[CONTEXT_KEY].get("env"), override_env=override_env
382
407
  )
383
408
  return ProjectProperties(project_definition, project_context)
409
+
410
+
411
+ def raw_project_properties(definition: Definition) -> ProjectProperties:
412
+ """
413
+ Returns the raw project definition data without any templating.
414
+ """
415
+ return ProjectProperties(build_project_definition(**definition), {})