snowflake-cli-labs 2.7.0rc4__py3-none-any.whl → 2.8.0rc0__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 (32) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/project/definition.py +3 -36
  3. snowflake/cli/api/project/schemas/entities/application_entity.py +5 -11
  4. snowflake/cli/api/project/schemas/entities/application_package_entity.py +5 -2
  5. snowflake/cli/api/project/schemas/entities/common.py +15 -22
  6. snowflake/cli/api/project/schemas/native_app/application.py +10 -2
  7. snowflake/cli/api/project/schemas/native_app/native_app.py +13 -2
  8. snowflake/cli/api/project/schemas/native_app/package.py +24 -1
  9. snowflake/cli/api/project/schemas/project_definition.py +23 -40
  10. snowflake/cli/api/project/schemas/snowpark/callable.py +1 -3
  11. snowflake/cli/api/project/schemas/updatable_model.py +148 -5
  12. snowflake/cli/api/project/util.py +55 -7
  13. snowflake/cli/api/rendering/jinja.py +1 -0
  14. snowflake/cli/api/rendering/project_templates.py +8 -7
  15. snowflake/cli/api/rendering/sql_templates.py +8 -4
  16. snowflake/cli/api/utils/definition_rendering.py +50 -11
  17. snowflake/cli/api/utils/models.py +10 -7
  18. snowflake/cli/api/utils/templating_functions.py +144 -0
  19. snowflake/cli/app/build_and_push.sh +8 -0
  20. snowflake/cli/app/snow_connector.py +14 -10
  21. snowflake/cli/plugins/init/commands.py +5 -3
  22. snowflake/cli/plugins/nativeapp/manager.py +81 -2
  23. snowflake/cli/plugins/nativeapp/project_model.py +13 -3
  24. snowflake/cli/plugins/nativeapp/run_processor.py +22 -51
  25. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +7 -18
  26. snowflake/cli/plugins/nativeapp/version/version_processor.py +4 -0
  27. snowflake/cli/plugins/snowpark/commands.py +6 -3
  28. {snowflake_cli_labs-2.7.0rc4.dist-info → snowflake_cli_labs-2.8.0rc0.dist-info}/METADATA +1 -1
  29. {snowflake_cli_labs-2.7.0rc4.dist-info → snowflake_cli_labs-2.8.0rc0.dist-info}/RECORD +32 -30
  30. {snowflake_cli_labs-2.7.0rc4.dist-info → snowflake_cli_labs-2.8.0rc0.dist-info}/WHEEL +0 -0
  31. {snowflake_cli_labs-2.7.0rc4.dist-info → snowflake_cli_labs-2.8.0rc0.dist-info}/entry_points.txt +0 -0
  32. {snowflake_cli_labs-2.7.0rc4.dist-info → snowflake_cli_labs-2.8.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -24,6 +24,7 @@ from jinja2 import Environment, StrictUndefined, loaders
24
24
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
25
25
 
26
26
  CONTEXT_KEY = "ctx"
27
+ FUNCTION_KEY = "fn"
27
28
 
28
29
 
29
30
  def read_file_content(file_name: str):
@@ -28,8 +28,10 @@ from snowflake.cli.api.exceptions import InvalidTemplate
28
28
  from snowflake.cli.api.rendering.jinja import IgnoreAttrEnvironment, env_bootstrap
29
29
  from snowflake.cli.api.secure_path import SecurePath
30
30
 
31
- _PROJECT_TEMPLATE_START = "<!"
32
- _PROJECT_TEMPLATE_END = "!>"
31
+ _VARIABLE_TEMPLATE_START = "<!"
32
+ _VARIABLE_TEMPLATE_END = "!>"
33
+ _BLOCK_TEMPLATE_START = "<!!"
34
+ _BLOCK_TEMPLATE_END = "!!>"
33
35
 
34
36
 
35
37
  def to_snowflake_identifier(value: Optional[str]) -> Optional[str]:
@@ -60,15 +62,14 @@ PROJECT_TEMPLATE_FILTERS = [to_snowflake_identifier]
60
62
 
61
63
 
62
64
  def get_template_cli_jinja_env(template_root: SecurePath) -> Environment:
63
- _random_block = "___very___unique___block___to___disable___logic___blocks___"
64
65
  env = env_bootstrap(
65
66
  IgnoreAttrEnvironment(
66
67
  loader=loaders.FileSystemLoader(searchpath=template_root.path),
67
68
  keep_trailing_newline=True,
68
- variable_start_string=_PROJECT_TEMPLATE_START,
69
- variable_end_string=_PROJECT_TEMPLATE_END,
70
- block_start_string=_random_block,
71
- block_end_string=_random_block,
69
+ variable_start_string=_VARIABLE_TEMPLATE_START,
70
+ variable_end_string=_VARIABLE_TEMPLATE_END,
71
+ block_start_string=_BLOCK_TEMPLATE_START,
72
+ block_end_string=_BLOCK_TEMPLATE_END,
72
73
  undefined=StrictUndefined,
73
74
  )
74
75
  )
@@ -21,12 +21,14 @@ from jinja2 import StrictUndefined, loaders
21
21
  from snowflake.cli.api.cli_global_context import cli_context
22
22
  from snowflake.cli.api.rendering.jinja import (
23
23
  CONTEXT_KEY,
24
+ FUNCTION_KEY,
24
25
  IgnoreAttrEnvironment,
25
26
  env_bootstrap,
26
27
  )
27
28
 
28
29
  _SQL_TEMPLATE_START = "&{"
29
30
  _SQL_TEMPLATE_END = "}"
31
+ RESERVED_KEYS = [CONTEXT_KEY, FUNCTION_KEY]
30
32
 
31
33
 
32
34
  def get_sql_cli_jinja_env(*, loader: Optional[loaders.BaseLoader] = None):
@@ -46,10 +48,12 @@ def get_sql_cli_jinja_env(*, loader: Optional[loaders.BaseLoader] = None):
46
48
 
47
49
  def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
48
50
  data = data or {}
49
- if CONTEXT_KEY in data:
50
- raise ClickException(
51
- f"{CONTEXT_KEY} in user defined data. The `{CONTEXT_KEY}` variable is reserved for CLI usage."
52
- )
51
+
52
+ for reserved_key in RESERVED_KEYS:
53
+ if reserved_key in data:
54
+ raise ClickException(
55
+ f"{reserved_key} in user defined data. The `{reserved_key}` variable is reserved for CLI usage."
56
+ )
53
57
 
54
58
  context_data = cli_context.template_context
55
59
  context_data.update(data)
@@ -25,13 +25,15 @@ from snowflake.cli.api.project.schemas.project_definition import (
25
25
  ProjectProperties,
26
26
  build_project_definition,
27
27
  )
28
+ from snowflake.cli.api.project.schemas.updatable_model import context
28
29
  from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
29
30
  from snowflake.cli.api.rendering.project_definition_templates import (
30
31
  get_project_definition_cli_jinja_env,
31
32
  )
32
- from snowflake.cli.api.utils.dict_utils import traverse
33
+ from snowflake.cli.api.utils.dict_utils import deep_merge_dicts, traverse
33
34
  from snowflake.cli.api.utils.graph import Graph, Node
34
35
  from snowflake.cli.api.utils.models import ProjectEnvironment
36
+ from snowflake.cli.api.utils.templating_functions import get_templating_functions
35
37
  from snowflake.cli.api.utils.types import Context, Definition
36
38
 
37
39
 
@@ -81,7 +83,17 @@ class TemplatedEnvironment:
81
83
  all_referenced_vars.add(TemplateVar(current_attr_chain))
82
84
  current_attr_chain = None
83
85
  elif (
84
- not isinstance(ast_node, (nodes.Template, nodes.TemplateData, nodes.Output))
86
+ not isinstance(
87
+ ast_node,
88
+ (
89
+ nodes.Template,
90
+ nodes.TemplateData,
91
+ nodes.Output,
92
+ nodes.Call,
93
+ nodes.Const,
94
+ nodes.Filter,
95
+ ),
96
+ )
85
97
  or current_attr_chain is not None
86
98
  ):
87
99
  raise InvalidTemplate(f"Unexpected templating syntax in {template_value}")
@@ -199,7 +211,6 @@ def _build_dependency_graph(
199
211
  dependencies_graph = Graph[TemplateVar]()
200
212
  for variable in all_vars:
201
213
  dependencies_graph.add(Node[TemplateVar](key=variable.key, data=variable))
202
-
203
214
  for variable in all_vars:
204
215
  # If variable is found in os.environ or from cli override, then use the value as is
205
216
  # skip rendering by pre-setting the rendered_value attribute
@@ -262,6 +273,22 @@ def _template_version_warning():
262
273
  )
263
274
 
264
275
 
276
+ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
277
+ with context({"skip_validation_on_templates": True}):
278
+ # pass a flag to Pydantic to skip validation for templated scalars
279
+ # populate the defaults
280
+ project_definition = build_project_definition(**original_definition)
281
+
282
+ definition_with_defaults = project_definition.model_dump(
283
+ exclude_none=True, warnings=False, by_alias=True
284
+ )
285
+ # The main purpose of the above operation was to populate defaults from Pydantic.
286
+ # By merging the original definition back in, we ensure that any transformations
287
+ # that Pydantic would have performed are undone.
288
+ deep_merge_dicts(definition_with_defaults, original_definition)
289
+ return definition_with_defaults
290
+
291
+
265
292
  def render_definition_template(
266
293
  original_definition: Optional[Definition], context_overrides: Context
267
294
  ) -> ProjectProperties:
@@ -276,11 +303,14 @@ def render_definition_template(
276
303
  Environment variables take precedence during the rendering process.
277
304
  """
278
305
 
279
- # protect input from update
306
+ # copy input to protect it from update
280
307
  definition = copy.deepcopy(original_definition)
281
308
 
282
- # start with an environment from overrides and environment variables:
309
+ # collect all the override --env variables passed through CLI input
283
310
  override_env = context_overrides.get(CONTEXT_KEY, {}).get("env", {})
311
+
312
+ # set up Project Environment with empty default_env because
313
+ # default env section from project definition file is still templated at this time
284
314
  environment_overrides = ProjectEnvironment(
285
315
  default_env={}, override_env=override_env
286
316
  )
@@ -288,7 +318,6 @@ def render_definition_template(
288
318
  if definition is None:
289
319
  return ProjectProperties(None, {CONTEXT_KEY: {"env": environment_overrides}})
290
320
 
291
- project_context = {CONTEXT_KEY: definition}
292
321
  template_env = TemplatedEnvironment(get_project_definition_cli_jinja_env())
293
322
 
294
323
  if "definition_version" not in definition or Version(
@@ -304,12 +333,18 @@ def render_definition_template(
304
333
  # also warn on Exception, as it means the user is incorrectly attempting to use templating
305
334
  _template_version_warning()
306
335
 
307
- project_definition = build_project_definition(**original_definition)
336
+ project_definition = build_project_definition(**definition)
337
+ project_context = {CONTEXT_KEY: definition}
308
338
  project_context[CONTEXT_KEY]["env"] = environment_overrides
309
339
  return ProjectProperties(project_definition, project_context)
310
340
 
311
- default_env = definition.get("env", {})
312
- _validate_env_section(default_env)
341
+ definition = _add_defaults_to_definition(definition)
342
+ project_context = {CONTEXT_KEY: definition}
343
+
344
+ _validate_env_section(definition.get("env", {}))
345
+
346
+ # add available templating functions
347
+ project_context["fn"] = get_templating_functions()
313
348
 
314
349
  referenced_vars = _get_referenced_vars_in_definition(template_env, definition)
315
350
 
@@ -338,7 +373,11 @@ def render_definition_template(
338
373
  update_action=lambda val: template_env.render(val, final_context),
339
374
  )
340
375
 
341
- definition["env"] = ProjectEnvironment(default_env, override_env)
342
- project_context[CONTEXT_KEY] = definition
343
376
  project_definition = build_project_definition(**definition)
377
+ project_context[CONTEXT_KEY] = definition
378
+ # Use `ProjectEnvironment` in project context in order to
379
+ # handle env variables overrides from OS env and from CLI arguments.
380
+ project_context[CONTEXT_KEY]["env"] = ProjectEnvironment(
381
+ default_env=project_context[CONTEXT_KEY].get("env"), override_env=override_env
382
+ )
344
383
  return ProjectProperties(project_definition, project_context)
@@ -15,12 +15,12 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import os
18
+ from dataclasses import dataclass
18
19
  from typing import Any, Dict, Optional
19
20
 
20
- from snowflake.cli.api.project.schemas.updatable_model import UpdatableModel
21
21
 
22
-
23
- class ProjectEnvironment(UpdatableModel):
22
+ @dataclass
23
+ class ProjectEnvironment:
24
24
  """
25
25
  This class handles retrieval of project env variables.
26
26
  These env variables can be accessed through templating, as ctx.env.<var_name>
@@ -31,13 +31,16 @@ class ProjectEnvironment(UpdatableModel):
31
31
  - Check for default values from the project definition file.
32
32
  """
33
33
 
34
- override_env: Dict[str, Any] = {}
35
- default_env: Dict[str, Any] = {}
34
+ override_env: Dict[str, Any]
35
+ default_env: Dict[str, Any]
36
36
 
37
37
  def __init__(
38
- self, default_env: Dict[str, Any], override_env: Optional[Dict[str, Any]] = None
38
+ self,
39
+ default_env: Optional[Dict[str, Any]] = None,
40
+ override_env: Optional[Dict[str, Any]] = None,
39
41
  ):
40
- super().__init__(self, default_env=default_env, override_env=override_env or {})
42
+ self.override_env = override_env or {}
43
+ self.default_env = default_env or {}
41
44
 
42
45
  def __getitem__(self, item):
43
46
  if item in self.override_env:
@@ -0,0 +1,144 @@
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, List, Optional
18
+
19
+ from snowflake.cli.api.exceptions import InvalidTemplate
20
+ from snowflake.cli.api.project.util import (
21
+ concat_identifiers,
22
+ get_env_username,
23
+ identifier_to_str,
24
+ sanitize_identifier,
25
+ to_identifier,
26
+ )
27
+
28
+
29
+ class TemplatingFunctions:
30
+ """
31
+ This class contains all the functions available for templating.
32
+ Any callable not starting with '_' will automatically be available for users to use.
33
+ """
34
+
35
+ @staticmethod
36
+ def _verify_str_arguments(
37
+ func_name: str,
38
+ args: List[Any],
39
+ *,
40
+ min_count: Optional[int] = None,
41
+ max_count: Optional[int] = None,
42
+ ):
43
+ if min_count is not None and len(args) < min_count:
44
+ raise InvalidTemplate(
45
+ f"{func_name} requires at least {min_count} argument(s)"
46
+ )
47
+
48
+ if max_count is not None and len(args) > max_count:
49
+ raise InvalidTemplate(
50
+ f"{func_name} supports at most {max_count} argument(s)"
51
+ )
52
+
53
+ for arg in args:
54
+ if not isinstance(arg, str):
55
+ raise InvalidTemplate(f"{func_name} only accepts String values")
56
+
57
+ @staticmethod
58
+ def concat_ids(*args):
59
+ """
60
+ input: one or more string arguments (SQL ID or plain String).
61
+ output: a valid SQL ID (quoted or unquoted)
62
+
63
+ Takes on multiple String arguments and concatenate them into one String.
64
+ If any of the Strings is a valid quoted ID, it will be unescaped for the concatenation process.
65
+ The resulting String is then escaped and quoted if:
66
+ - It contains non SQL safe characters
67
+ - Any of the input was a valid quoted identifier.
68
+ """
69
+ TemplatingFunctions._verify_str_arguments("concat_ids", args, min_count=1)
70
+ return concat_identifiers(args)
71
+
72
+ @staticmethod
73
+ def str_to_id(*args):
74
+ """
75
+ input: one string argument. (SQL ID or plain String)
76
+ output: a valid SQL ID (quoted or unquoted)
77
+
78
+ If the input is a valid quoted or valid unquoted identifier, return it as is.
79
+ Otherwise, if the input contains unsafe characters and is not properly quoted,
80
+ then escape it and quote it.
81
+ """
82
+ TemplatingFunctions._verify_str_arguments(
83
+ "str_to_id", args, min_count=1, max_count=1
84
+ )
85
+ return to_identifier(args[0])
86
+
87
+ @staticmethod
88
+ def id_to_str(*args):
89
+ """
90
+ input: one string argument (SQL ID or plain String).
91
+ output: a plain string
92
+
93
+ If the input is a valid SQL ID, then unescape it and return the plain String version.
94
+ Otherwise, return the input as is.
95
+ """
96
+ TemplatingFunctions._verify_str_arguments(
97
+ "id_to_str", args, min_count=1, max_count=1
98
+ )
99
+ return identifier_to_str(args[0])
100
+
101
+ @staticmethod
102
+ def get_username(*args):
103
+ """
104
+ input: one optional string containing the fallback value
105
+ output: current username detected from the Operating System
106
+
107
+ If the current username is not found or is blank, return blank
108
+ or use the fallback value if provided.
109
+ """
110
+ TemplatingFunctions._verify_str_arguments(
111
+ "get_username", args, min_count=0, max_count=1
112
+ )
113
+ fallback_username = args[0] if len(args) > 0 else ""
114
+ return get_env_username() or fallback_username
115
+
116
+ @staticmethod
117
+ def sanitize_id(*args):
118
+ """
119
+ input: one string argument
120
+ output: a valid non-quoted SQL ID
121
+
122
+ Removes any unsafe SQL characters from the input,
123
+ prepend it with an underscore if it does not start with a valid character,
124
+ and limit the result to 255 characters.
125
+ The result is a valid unquoted SQL ID.
126
+ """
127
+ TemplatingFunctions._verify_str_arguments(
128
+ "sanitize_id", args, min_count=1, max_count=1
129
+ )
130
+
131
+ return sanitize_identifier(args[0])
132
+
133
+
134
+ def get_templating_functions():
135
+ """
136
+ Returns a dictionary with all the functions available for templating
137
+ """
138
+ templating_functions = {
139
+ func: getattr(TemplatingFunctions, func)
140
+ for func in dir(TemplatingFunctions)
141
+ if callable(getattr(TemplatingFunctions, func)) and not func.startswith("_")
142
+ }
143
+
144
+ return templating_functions
@@ -0,0 +1,8 @@
1
+ set -e
2
+ export SF_REGISTRY="$(snow spcs image-registry url -c integration)"
3
+ DATABASE=$(echo "${SNOWFLAKE_CONNECTIONS_INTEGRATION_DATABASE}" | tr '[:upper:]' '[:lower:]')
4
+
5
+ echo "Using registry: ${SF_REGISTRY}"
6
+ docker build --platform linux/amd64 -t "${SF_REGISTRY}/${DATABASE}/public/snowcli_repository/test_counter" .
7
+ snow spcs image-registry token --format=json -c integration | docker login "${SF_REGISTRY}/${DATABASE}/public/snowcli_repository" -u 0sessiontoken --password-stdin
8
+ docker push "${SF_REGISTRY}/${DATABASE}/public/snowcli_repository/test_counter"
@@ -97,7 +97,7 @@ def connect_to_snowflake(
97
97
  k: v for k, v in connection_parameters.items() if v is not None
98
98
  }
99
99
 
100
- connection_parameters = _update_connection_details_with_private_key(
100
+ connection_parameters = update_connection_details_with_private_key(
101
101
  connection_parameters
102
102
  )
103
103
 
@@ -163,7 +163,7 @@ def _raise_errors_related_to_session_token(
163
163
  )
164
164
 
165
165
 
166
- def _update_connection_details_with_private_key(connection_parameters: Dict):
166
+ def update_connection_details_with_private_key(connection_parameters: Dict):
167
167
  if "private_key_path" in connection_parameters:
168
168
  if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
169
169
  private_key = _load_pem_to_der(connection_parameters["private_key_path"])
@@ -189,13 +189,6 @@ def _load_pem_to_der(private_key_path: str) -> bytes:
189
189
  Given a private key file path (in PEM format), decode key data into DER
190
190
  format
191
191
  """
192
- from cryptography.hazmat.backends import default_backend
193
- from cryptography.hazmat.primitives.serialization import (
194
- Encoding,
195
- NoEncryption,
196
- PrivateFormat,
197
- load_pem_private_key,
198
- )
199
192
 
200
193
  with SecurePath(private_key_path).open(
201
194
  "rb", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
@@ -222,6 +215,18 @@ def _load_pem_to_der(private_key_path: str) -> bytes:
222
215
  if private_key_pem.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
223
216
  private_key_passphrase = None
224
217
 
218
+ return prepare_private_key(private_key_pem, private_key_passphrase)
219
+
220
+
221
+ def prepare_private_key(private_key_pem, private_key_passphrase=None):
222
+ from cryptography.hazmat.backends import default_backend
223
+ from cryptography.hazmat.primitives.serialization import (
224
+ Encoding,
225
+ NoEncryption,
226
+ PrivateFormat,
227
+ load_pem_private_key,
228
+ )
229
+
225
230
  private_key = load_pem_private_key(
226
231
  private_key_pem,
227
232
  (
@@ -231,7 +236,6 @@ def _load_pem_to_der(private_key_path: str) -> bytes:
231
236
  ),
232
237
  default_backend(),
233
238
  )
234
-
235
239
  return private_key.private_bytes(
236
240
  encoding=Encoding.DER,
237
241
  format=PrivateFormat.PKCS8,
@@ -20,6 +20,7 @@ from typing import Any, Dict, List, Optional
20
20
  import typer
21
21
  import yaml
22
22
  from click import ClickException
23
+ from snowflake.cli.__about__ import VERSION
23
24
  from snowflake.cli.api.commands.flags import (
24
25
  NoInteractiveOption,
25
26
  parse_key_value_variables,
@@ -176,7 +177,6 @@ def _determine_variable_values(
176
177
 
177
178
  def _validate_cli_version(required_version: str) -> None:
178
179
  from packaging.version import parse
179
- from snowflake.cli.__about__ import VERSION
180
180
 
181
181
  if parse(required_version) > parse(VERSION):
182
182
  raise ClickException(
@@ -225,8 +225,10 @@ def init(
225
225
  variables_metadata=template_metadata.variables,
226
226
  variables_from_flags=variables_from_flags,
227
227
  no_interactive=no_interactive,
228
- )
229
- variable_values["project_dir_name"] = SecurePath(path).name
228
+ ) | {
229
+ "project_dir_name": SecurePath(path).name,
230
+ "snowflake_cli_version": VERSION,
231
+ }
230
232
  log.debug(
231
233
  "Rendering template files: %s", ", ".join(template_metadata.files_to_render)
232
234
  )
@@ -25,6 +25,7 @@ from typing import Any, List, NoReturn, Optional, TypedDict
25
25
 
26
26
  import jinja2
27
27
  from click import ClickException
28
+ from snowflake.cli.api.cli_global_context import cli_context
28
29
  from snowflake.cli.api.console import cli_console as cc
29
30
  from snowflake.cli.api.errno import (
30
31
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
@@ -33,7 +34,7 @@ from snowflake.cli.api.errno import (
33
34
  )
34
35
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
35
36
  from snowflake.cli.api.project.schemas.native_app.application import (
36
- ApplicationPostDeployHook,
37
+ PostDeployHook,
37
38
  )
38
39
  from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
39
40
  from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
@@ -41,6 +42,9 @@ from snowflake.cli.api.project.util import (
41
42
  identifier_for_url,
42
43
  unquote_identifier,
43
44
  )
45
+ from snowflake.cli.api.rendering.sql_templates import (
46
+ get_sql_cli_jinja_env,
47
+ )
44
48
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
45
49
  from snowflake.cli.plugins.connection.util import make_snowsight_url
46
50
  from snowflake.cli.plugins.nativeapp.artifacts import (
@@ -279,9 +283,13 @@ class NativeAppManager(SqlExecutionMixin):
279
283
  return self.na_project.app_role
280
284
 
281
285
  @property
282
- def app_post_deploy_hooks(self) -> Optional[List[ApplicationPostDeployHook]]:
286
+ def app_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
283
287
  return self.na_project.app_post_deploy_hooks
284
288
 
289
+ @property
290
+ def package_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
291
+ return self.na_project.package_post_deploy_hooks
292
+
285
293
  @property
286
294
  def debug_mode(self) -> bool:
287
295
  return self.na_project.debug_mode
@@ -603,6 +611,12 @@ class NativeAppManager(SqlExecutionMixin):
603
611
  Assuming the application package exists and we are using the correct role,
604
612
  applies all package scripts in-order to the application package.
605
613
  """
614
+
615
+ if self.package_scripts:
616
+ cc.warning(
617
+ "WARNING: native_app.package.scripts is deprecated. Please migrate to using native_app.package.post_deploy."
618
+ )
619
+
606
620
  env = jinja2.Environment(
607
621
  loader=jinja2.loaders.FileSystemLoader(self.project_root),
608
622
  keep_trailing_newline=True,
@@ -624,6 +638,67 @@ class NativeAppManager(SqlExecutionMixin):
624
638
  err, role=self.package_role, warehouse=self.package_warehouse
625
639
  )
626
640
 
641
+ def _execute_sql_script(
642
+ self, script_content: str, database_name: Optional[str] = None
643
+ ) -> None:
644
+ """
645
+ Executing the provided SQL script content.
646
+ This assumes that a relevant warehouse is already active.
647
+ If database_name is passed in, it will be used first.
648
+ """
649
+ try:
650
+ if database_name is not None:
651
+ self._execute_query(f"use database {database_name}")
652
+
653
+ self._execute_queries(script_content)
654
+ except ProgrammingError as err:
655
+ generic_sql_error_handler(err)
656
+
657
+ def _execute_post_deploy_hooks(
658
+ self,
659
+ post_deploy_hooks: Optional[List[PostDeployHook]],
660
+ deployed_object_type: str,
661
+ database_name: str,
662
+ ) -> None:
663
+ """
664
+ Executes post-deploy hooks for the given object type.
665
+ While executing SQL post deploy hooks, it first switches to the database provided in the input.
666
+ All post deploy scripts templates will first be expanded using the global template context.
667
+ """
668
+ if not post_deploy_hooks:
669
+ return
670
+
671
+ with cc.phase(f"Executing {deployed_object_type} post-deploy actions"):
672
+ sql_scripts_paths = []
673
+ for hook in post_deploy_hooks:
674
+ if hook.sql_script:
675
+ sql_scripts_paths.append(hook.sql_script)
676
+ else:
677
+ raise ValueError(
678
+ f"Unsupported {deployed_object_type} post-deploy hook type: {hook}"
679
+ )
680
+
681
+ env = get_sql_cli_jinja_env(
682
+ loader=jinja2.loaders.FileSystemLoader(self.project_root)
683
+ )
684
+ scripts_content_list = self._expand_script_templates(
685
+ env, cli_context.template_context, sql_scripts_paths
686
+ )
687
+
688
+ for index, sql_script_path in enumerate(sql_scripts_paths):
689
+ cc.step(f"Executing SQL script: {sql_script_path}")
690
+ self._execute_sql_script(scripts_content_list[index], database_name)
691
+
692
+ def execute_package_post_deploy_hooks(self) -> None:
693
+ self._execute_post_deploy_hooks(
694
+ self.package_post_deploy_hooks, "application package", self.package_name
695
+ )
696
+
697
+ def execute_app_post_deploy_hooks(self) -> None:
698
+ self._execute_post_deploy_hooks(
699
+ self.app_post_deploy_hooks, "application", self.app_name
700
+ )
701
+
627
702
  def deploy(
628
703
  self,
629
704
  bundle_map: BundleMap,
@@ -655,6 +730,10 @@ class NativeAppManager(SqlExecutionMixin):
655
730
  print_diff=print_diff,
656
731
  )
657
732
 
733
+ # 4. Execute post-deploy hooks
734
+ with self.use_package_warehouse():
735
+ self.execute_package_post_deploy_hooks()
736
+
658
737
  if validate:
659
738
  self.validate(use_scratch_stage=False)
660
739
 
@@ -25,7 +25,7 @@ from snowflake.cli.api.project.definition import (
25
25
  default_role,
26
26
  )
27
27
  from snowflake.cli.api.project.schemas.native_app.application import (
28
- ApplicationPostDeployHook,
28
+ PostDeployHook,
29
29
  )
30
30
  from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
31
31
  from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
@@ -162,15 +162,25 @@ class NativeAppProjectModel:
162
162
  return self._default_role
163
163
 
164
164
  @cached_property
165
- def app_post_deploy_hooks(self) -> Optional[List[ApplicationPostDeployHook]]:
165
+ def app_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
166
166
  """
167
- List of application post deploy hooks.
167
+ List of application instance post deploy hooks.
168
168
  """
169
169
  if self.definition.application and self.definition.application.post_deploy:
170
170
  return self.definition.application.post_deploy
171
171
  else:
172
172
  return None
173
173
 
174
+ @cached_property
175
+ def package_post_deploy_hooks(self) -> Optional[List[PostDeployHook]]:
176
+ """
177
+ List of application package post deploy hooks.
178
+ """
179
+ if self.definition.package and self.definition.package.post_deploy:
180
+ return self.definition.package.post_deploy
181
+ else:
182
+ return None
183
+
174
184
  @cached_property
175
185
  def _default_role(self) -> str:
176
186
  role = default_role()