snowflake-cli-labs 2.8.0rc1__py3-none-any.whl → 3.0.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 (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 +79 -26
  27. snowflake/cli/{plugins → _plugins}/git/manager.py +72 -17
  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 +10 -6
  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 +8 -6
  59. snowflake/cli/{plugins → _plugins}/notebook/manager.py +14 -14
  60. snowflake/cli/{plugins/nativeapp → _plugins/notebook}/plugin_spec.py +1 -1
  61. snowflake/cli/{plugins → _plugins}/notebook/types.py +0 -1
  62. snowflake/cli/{plugins → _plugins}/object/command_aliases.py +6 -5
  63. snowflake/cli/{plugins → _plugins}/object/commands.py +16 -10
  64. snowflake/cli/{plugins → _plugins}/object/manager.py +43 -21
  65. snowflake/cli/{plugins → _plugins}/object/plugin_spec.py +1 -1
  66. snowflake/cli/_plugins/snowpark/commands.py +450 -0
  67. snowflake/cli/_plugins/snowpark/common.py +268 -0
  68. snowflake/cli/{plugins → _plugins}/snowpark/models.py +2 -8
  69. snowflake/cli/{plugins → _plugins}/snowpark/package/anaconda_packages.py +2 -36
  70. snowflake/cli/{plugins → _plugins}/snowpark/package/commands.py +13 -74
  71. snowflake/cli/{plugins → _plugins}/snowpark/package/manager.py +4 -3
  72. snowflake/cli/{plugins → _plugins}/snowpark/package_utils.py +5 -5
  73. snowflake/cli/_plugins/snowpark/plugin_spec.py +30 -0
  74. snowflake/cli/_plugins/snowpark/snowpark_entity.py +29 -0
  75. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +173 -0
  76. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +109 -0
  77. snowflake/cli/{plugins → _plugins}/snowpark/snowpark_shared.py +0 -36
  78. snowflake/cli/{plugins → _plugins}/snowpark/zipper.py +16 -8
  79. snowflake/cli/{plugins → _plugins}/spcs/__init__.py +5 -7
  80. snowflake/cli/{plugins → _plugins}/spcs/compute_pool/commands.py +29 -28
  81. snowflake/cli/{plugins → _plugins}/spcs/compute_pool/manager.py +3 -3
  82. snowflake/cli/{plugins → _plugins}/spcs/image_registry/commands.py +3 -3
  83. snowflake/cli/{plugins → _plugins}/spcs/image_repository/commands.py +25 -19
  84. snowflake/cli/{plugins → _plugins}/spcs/image_repository/manager.py +1 -1
  85. snowflake/cli/{plugins → _plugins}/spcs/plugin_spec.py +1 -1
  86. snowflake/cli/{plugins → _plugins}/spcs/services/commands.py +66 -32
  87. snowflake/cli/{plugins → _plugins}/spcs/services/manager.py +43 -5
  88. snowflake/cli/{plugins → _plugins}/sql/commands.py +20 -17
  89. snowflake/cli/{plugins → _plugins}/sql/manager.py +1 -1
  90. snowflake/cli/{plugins → _plugins}/sql/plugin_spec.py +1 -1
  91. snowflake/cli/{plugins → _plugins}/stage/commands.py +20 -17
  92. snowflake/cli/{plugins → _plugins}/stage/diff.py +1 -47
  93. snowflake/cli/{plugins → _plugins}/stage/manager.py +62 -24
  94. snowflake/cli/{plugins → _plugins}/stage/plugin_spec.py +1 -1
  95. snowflake/cli/_plugins/stage/utils.py +54 -0
  96. snowflake/cli/{plugins → _plugins}/streamlit/commands.py +71 -62
  97. snowflake/cli/{plugins → _plugins}/streamlit/manager.py +68 -70
  98. snowflake/cli/_plugins/streamlit/plugin_spec.py +30 -0
  99. snowflake/cli/_plugins/streamlit/streamlit_entity.py +12 -0
  100. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +66 -0
  101. snowflake/cli/_plugins/workspace/action_context.py +18 -0
  102. snowflake/cli/_plugins/workspace/commands.py +306 -0
  103. snowflake/cli/_plugins/workspace/manager.py +74 -0
  104. snowflake/cli/_plugins/workspace/plugin_spec.py +30 -0
  105. snowflake/cli/api/cli_global_context.py +152 -295
  106. snowflake/cli/api/commands/common.py +25 -0
  107. snowflake/cli/api/commands/decorators.py +19 -4
  108. snowflake/cli/api/commands/experimental_behaviour.py +2 -3
  109. snowflake/cli/api/commands/flags.py +143 -222
  110. snowflake/cli/api/commands/overrideable_parameter.py +143 -0
  111. snowflake/cli/api/commands/snow_typer.py +21 -11
  112. snowflake/cli/api/commands/utils.py +18 -0
  113. snowflake/cli/api/config.py +44 -12
  114. snowflake/cli/api/connections.py +216 -0
  115. snowflake/cli/api/console/abc.py +8 -3
  116. snowflake/cli/api/constants.py +11 -0
  117. snowflake/cli/api/entities/common.py +56 -0
  118. snowflake/cli/api/entities/utils.py +370 -0
  119. snowflake/cli/api/errno.py +1 -0
  120. snowflake/cli/api/exceptions.py +31 -5
  121. snowflake/cli/api/feature_flags.py +0 -1
  122. snowflake/cli/api/identifiers.py +45 -9
  123. snowflake/cli/api/metrics.py +92 -0
  124. snowflake/cli/api/project/definition.py +48 -6
  125. snowflake/cli/api/project/definition_conversion.py +400 -0
  126. snowflake/cli/api/project/definition_manager.py +16 -5
  127. snowflake/cli/api/project/project_verification.py +3 -3
  128. snowflake/cli/api/project/schemas/entities/common.py +91 -16
  129. snowflake/cli/api/project/schemas/entities/entities.py +37 -6
  130. snowflake/cli/api/project/schemas/project_definition.py +180 -49
  131. snowflake/cli/api/project/schemas/updatable_model.py +11 -3
  132. snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
  133. snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +3 -1
  134. snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
  135. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +8 -9
  136. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
  137. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +7 -1
  138. snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
  139. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
  140. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
  141. snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
  142. snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
  143. snowflake/cli/api/project/util.py +23 -6
  144. snowflake/cli/api/rendering/jinja.py +14 -8
  145. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  146. snowflake/cli/api/rendering/sql_templates.py +56 -11
  147. snowflake/cli/api/rest_api.py +11 -5
  148. snowflake/cli/api/secure_path.py +16 -18
  149. snowflake/cli/api/secure_utils.py +90 -1
  150. snowflake/cli/api/sql_execution.py +47 -27
  151. snowflake/cli/api/utils/definition_rendering.py +45 -13
  152. {snowflake_cli_labs-2.8.0rc1.dist-info → snowflake_cli_labs-3.0.0.dist-info}/METADATA +20 -18
  153. snowflake_cli_labs-3.0.0.dist-info/RECORD +242 -0
  154. snowflake_cli_labs-3.0.0.dist-info/entry_points.txt +2 -0
  155. snowflake/cli/api/commands/project_initialisation.py +0 -65
  156. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  157. snowflake/cli/api/project/schemas/entities/application_entity.py +0 -44
  158. snowflake/cli/api/project/schemas/entities/application_package_entity.py +0 -66
  159. snowflake/cli/app/build_and_push.sh +0 -8
  160. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +0 -172
  161. snowflake/cli/plugins/nativeapp/init.py +0 -345
  162. snowflake/cli/plugins/nativeapp/manager.py +0 -823
  163. snowflake/cli/plugins/nativeapp/run_processor.py +0 -389
  164. snowflake/cli/plugins/nativeapp/teardown_processor.py +0 -301
  165. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +0 -135
  166. snowflake/cli/plugins/nativeapp/version/version_processor.py +0 -362
  167. snowflake/cli/plugins/object_stage_deprecated/__init__.py +0 -15
  168. snowflake/cli/plugins/object_stage_deprecated/commands.py +0 -122
  169. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +0 -32
  170. snowflake/cli/plugins/snowpark/commands.py +0 -548
  171. snowflake/cli/plugins/snowpark/common.py +0 -307
  172. snowflake/cli/plugins/snowpark/manager.py +0 -109
  173. snowflake/cli/plugins/snowpark/plugin_spec.py +0 -30
  174. snowflake/cli/plugins/snowpark/snowpark_package_paths.py +0 -65
  175. snowflake/cli/plugins/spcs/jobs/commands.py +0 -78
  176. snowflake/cli/plugins/spcs/jobs/manager.py +0 -53
  177. snowflake/cli/plugins/streamlit/__init__.py +0 -13
  178. snowflake/cli/plugins/streamlit/plugin_spec.py +0 -30
  179. snowflake/cli/plugins/workspace/__init__.py +0 -13
  180. snowflake/cli/plugins/workspace/commands.py +0 -35
  181. snowflake/cli/plugins/workspace/plugin_spec.py +0 -30
  182. snowflake/cli/templates/default_snowpark/.gitignore +0 -4
  183. snowflake/cli/templates/default_snowpark/app/common.py +0 -2
  184. snowflake/cli/templates/default_snowpark/app/functions.py +0 -15
  185. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -22
  186. snowflake/cli/templates/default_snowpark/requirements.txt +0 -1
  187. snowflake/cli/templates/default_snowpark/snowflake.yml +0 -23
  188. snowflake/cli/templates/default_streamlit/.gitignore +0 -4
  189. snowflake/cli/templates/default_streamlit/common/hello.py +0 -2
  190. snowflake/cli/templates/default_streamlit/environment.yml +0 -6
  191. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -3
  192. snowflake/cli/templates/default_streamlit/snowflake.yml +0 -10
  193. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -4
  194. snowflake_cli_labs-2.8.0rc1.dist-info/RECORD +0 -240
  195. snowflake_cli_labs-2.8.0rc1.dist-info/entry_points.txt +0 -2
  196. /snowflake/cli/{app → _app}/__init__.py +0 -0
  197. /snowflake/cli/{api/project/schemas/native_app → _app/api_impl}/__init__.py +0 -0
  198. /snowflake/cli/{api/project/schemas/snowpark → _app/api_impl/plugin}/__init__.py +0 -0
  199. /snowflake/cli/{app → _app}/api_impl/plugin/plugin_config_provider_impl.py +0 -0
  200. /snowflake/cli/{app → _app}/commands_registration/__init__.py +0 -0
  201. /snowflake/cli/{app → _app}/commands_registration/threadsafe.py +0 -0
  202. /snowflake/cli/{app → _app}/constants.py +0 -0
  203. /snowflake/cli/{api/project/schemas/streamlit → _app/dev}/__init__.py +0 -0
  204. /snowflake/cli/{app → _app}/dev/commands_structure.py +0 -0
  205. /snowflake/cli/{app/api_impl → _app/dev/docs}/__init__.py +0 -0
  206. /snowflake/cli/{app → _app}/dev/docs/project_definition_generate_json_schema.py +0 -0
  207. /snowflake/cli/{app → _app}/dev/docs/template_utils.py +0 -0
  208. /snowflake/cli/{app → _app}/dev/docs/templates/definition_description.rst.jinja2 +0 -0
  209. /snowflake/cli/{app → _app}/dev/docs/templates/overview.rst.jinja2 +0 -0
  210. /snowflake/cli/{app → _app}/dev/pycharm_remote_debug.py +0 -0
  211. /snowflake/cli/{app → _app}/loggers.py +0 -0
  212. /snowflake/cli/{app/api_impl/plugin → _plugins}/__init__.py +0 -0
  213. /snowflake/cli/{app/dev → _plugins/connection}/__init__.py +0 -0
  214. /snowflake/cli/{app/dev/docs → _plugins/cortex}/__init__.py +0 -0
  215. /snowflake/cli/{plugins → _plugins}/cortex/types.py +0 -0
  216. /snowflake/cli/{plugins → _plugins/git}/__init__.py +0 -0
  217. /snowflake/cli/{plugins/connection → _plugins/helpers}/__init__.py +0 -0
  218. /snowflake/cli/{plugins/cortex → _plugins/init}/__init__.py +0 -0
  219. /snowflake/cli/{plugins/git → _plugins/nativeapp}/__init__.py +0 -0
  220. /snowflake/cli/{plugins/init → _plugins/nativeapp/codegen}/__init__.py +0 -0
  221. /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/sandbox.py +0 -0
  222. /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/callback_source.py.jinja +0 -0
  223. /snowflake/cli/{plugins → _plugins}/nativeapp/constants.py +0 -0
  224. /snowflake/cli/{templates/default_snowpark/app → _plugins/nativeapp/entities}/__init__.py +0 -0
  225. /snowflake/cli/{plugins → _plugins}/nativeapp/feature_flags.py +0 -0
  226. /snowflake/cli/{plugins → _plugins}/nativeapp/utils.py +0 -0
  227. /snowflake/cli/{plugins/nativeapp → _plugins/nativeapp/version}/__init__.py +0 -0
  228. /snowflake/cli/{plugins/nativeapp/codegen → _plugins/notebook}/__init__.py +0 -0
  229. /snowflake/cli/{plugins → _plugins}/notebook/exceptions.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_labs-2.8.0rc1.dist-info → snowflake_cli_labs-3.0.0.dist-info}/WHEEL +0 -0
  251. {snowflake_cli_labs-2.8.0rc1.dist-info → snowflake_cli_labs-3.0.0.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
 
@@ -147,11 +151,11 @@ class SqlExecutionMixin:
147
151
  self.use(object_type=ObjectType.WAREHOUSE, name=prev_wh)
148
152
 
149
153
  def create_password_secret(
150
- self, name: str, username: str, password: str
154
+ self, name: FQN, username: str, password: str
151
155
  ) -> SnowflakeCursor:
152
156
  return self._execute_query(
153
157
  f"""
154
- create secret {name}
158
+ create secret {name.sql_identifier}
155
159
  type = password
156
160
  username = '{username}'
157
161
  password = '{password}'
@@ -159,11 +163,11 @@ class SqlExecutionMixin:
159
163
  )
160
164
 
161
165
  def create_api_integration(
162
- self, name: str, api_provider: str, allowed_prefix: str, secret: Optional[str]
166
+ self, name: FQN, api_provider: str, allowed_prefix: str, secret: Optional[str]
163
167
  ) -> SnowflakeCursor:
164
168
  return self._execute_query(
165
169
  f"""
166
- create api integration {name}
170
+ create api integration {name.sql_identifier}
167
171
  api_provider = {api_provider}
168
172
  api_allowed_prefixes = ('{allowed_prefix}')
169
173
  allowed_authentication_secrets = ({secret if secret else ''})
@@ -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)