snowflake-cli 3.2.2__py3-none-any.whl → 3.4.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 (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_app/constants.py +4 -0
  6. snowflake/cli/_app/snow_connector.py +12 -0
  7. snowflake/cli/_app/telemetry.py +10 -3
  8. snowflake/cli/_plugins/connection/util.py +12 -19
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +207 -1
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
  19. snowflake/cli/_plugins/nativeapp/commands.py +113 -21
  20. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
  23. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  24. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  25. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  26. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
  27. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  28. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
  29. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  30. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  31. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
  32. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
  35. snowflake/cli/_plugins/notebook/commands.py +55 -2
  36. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  37. snowflake/cli/_plugins/notebook/manager.py +7 -5
  38. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  39. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  40. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  41. snowflake/cli/_plugins/notebook/types.py +3 -0
  42. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  43. snowflake/cli/_plugins/snowpark/common.py +47 -2
  44. snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
  45. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  46. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  47. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  48. snowflake/cli/_plugins/spcs/common.py +129 -0
  49. snowflake/cli/_plugins/spcs/services/commands.py +131 -14
  50. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  51. snowflake/cli/_plugins/stage/commands.py +2 -1
  52. snowflake/cli/_plugins/stage/diff.py +60 -39
  53. snowflake/cli/_plugins/stage/manager.py +34 -13
  54. snowflake/cli/_plugins/stage/utils.py +1 -1
  55. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  56. snowflake/cli/_plugins/streamlit/manager.py +70 -22
  57. snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
  58. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  59. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  60. snowflake/cli/_plugins/workspace/commands.py +6 -5
  61. snowflake/cli/_plugins/workspace/manager.py +9 -5
  62. snowflake/cli/api/artifacts/__init__.py +13 -0
  63. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  64. snowflake/cli/api/artifacts/common.py +78 -0
  65. snowflake/cli/api/artifacts/utils.py +82 -0
  66. snowflake/cli/api/cli_global_context.py +36 -2
  67. snowflake/cli/api/commands/flags.py +10 -4
  68. snowflake/cli/api/commands/utils.py +28 -2
  69. snowflake/cli/api/config.py +6 -2
  70. snowflake/cli/api/connections.py +12 -1
  71. snowflake/cli/api/constants.py +10 -1
  72. snowflake/cli/api/entities/common.py +81 -14
  73. snowflake/cli/api/entities/resolver.py +160 -0
  74. snowflake/cli/api/entities/utils.py +65 -23
  75. snowflake/cli/api/errno.py +63 -3
  76. snowflake/cli/api/feature_flags.py +19 -4
  77. snowflake/cli/api/metrics.py +21 -27
  78. snowflake/cli/api/project/definition_conversion.py +4 -4
  79. snowflake/cli/api/project/project_paths.py +28 -0
  80. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  81. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  82. snowflake/cli/api/project/schemas/project_definition.py +54 -6
  83. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  84. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  85. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  86. snowflake/cli/api/project/util.py +45 -0
  87. snowflake/cli/api/secure_path.py +6 -0
  88. snowflake/cli/api/sql_execution.py +5 -1
  89. snowflake/cli/api/stage_path.py +7 -2
  90. snowflake/cli/api/utils/graph.py +3 -0
  91. snowflake/cli/api/utils/path_utils.py +24 -0
  92. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
  93. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
  94. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
  95. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  96. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import re
6
+ from datetime import datetime
7
+ from functools import cached_property
5
8
  from pathlib import Path
6
9
  from textwrap import dedent
7
- from typing import List, Literal, Optional, Union
10
+ from typing import Any, List, Literal, Optional, Set, Union
8
11
 
9
12
  import typer
10
- from click import BadOptionUsage, ClickException
13
+ from click import BadOptionUsage, ClickException, UsageError
11
14
  from pydantic import Field, field_validator
15
+ from snowflake.cli._plugins.connection.util import UIParameter
12
16
  from snowflake.cli._plugins.nativeapp.artifacts import (
13
- BundleMap,
14
17
  VersionInfo,
15
18
  build_bundle,
19
+ find_setup_script_file,
16
20
  find_version_info_in_manifest_file,
17
21
  )
18
22
  from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
@@ -20,14 +24,19 @@ from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler
20
24
  from snowflake.cli._plugins.nativeapp.constants import (
21
25
  ALLOWED_SPECIAL_COMMENTS,
22
26
  COMMENT_COL,
27
+ DEFAULT_CHANNEL,
28
+ DEFAULT_DIRECTIVE,
23
29
  EXTERNAL_DISTRIBUTION,
24
30
  INTERNAL_DISTRIBUTION,
31
+ MAX_VERSIONS_IN_RELEASE_CHANNEL,
25
32
  NAME_COL,
26
33
  OWNER_COL,
27
34
  PATCH_COL,
28
- SPECIAL_COMMENT,
29
35
  VERSION_COL,
30
36
  )
37
+ from snowflake.cli._plugins.nativeapp.entities.application_package_child_interface import (
38
+ ApplicationPackageChildInterface,
39
+ )
31
40
  from snowflake.cli._plugins.nativeapp.exceptions import (
32
41
  ApplicationPackageAlreadyExistsError,
33
42
  ApplicationPackageDoesNotExistError,
@@ -35,6 +44,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
35
44
  ObjectPropertyNotFoundError,
36
45
  SetupScriptFailedValidation,
37
46
  )
47
+ from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
38
48
  from snowflake.cli._plugins.nativeapp.policy import (
39
49
  AllowAlwaysPolicy,
40
50
  AskAlwaysPolicy,
@@ -45,36 +55,57 @@ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
45
55
  from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
46
56
  InsufficientPrivilegesError,
47
57
  )
48
- from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
49
- from snowflake.cli._plugins.stage.diff import DiffResult
50
- from snowflake.cli._plugins.stage.manager import StageManager
58
+ from snowflake.cli._plugins.nativeapp.sf_sql_facade import ReleaseChannel, Version
59
+ from snowflake.cli._plugins.nativeapp.utils import needs_confirmation, sanitize_dir_name
60
+ from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
61
+ FunctionEntityModel,
62
+ ProcedureEntityModel,
63
+ )
64
+ from snowflake.cli._plugins.stage.diff import DiffResult, compute_stage_diff
65
+ from snowflake.cli._plugins.stage.manager import (
66
+ DefaultStagePathParts,
67
+ StageManager,
68
+ StagePathParts,
69
+ )
70
+ from snowflake.cli._plugins.stage.utils import print_diff_to_console
71
+ from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
72
+ StreamlitEntityModel,
73
+ )
51
74
  from snowflake.cli._plugins.workspace.context import ActionContext
52
- from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
75
+ from snowflake.cli.api.artifacts.bundle_map import BundleMap
76
+ from snowflake.cli.api.cli_global_context import span
77
+ from snowflake.cli.api.entities.common import (
78
+ EntityBase,
79
+ attach_spans_to_entity_actions,
80
+ )
53
81
  from snowflake.cli.api.entities.utils import (
54
82
  drop_generic_object,
55
83
  execute_post_deploy_hooks,
56
84
  generic_sql_error_handler,
85
+ get_sql_executor,
57
86
  sync_deploy_root_with_stage,
58
87
  validation_item_to_str,
59
88
  )
60
89
  from snowflake.cli.api.errno import DOES_NOT_EXIST_OR_NOT_AUTHORIZED
61
90
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
62
91
  from snowflake.cli.api.project.schemas.entities.common import (
63
- EntityModelBase,
92
+ EntityModelBaseWithArtifacts,
64
93
  Identifier,
65
94
  PostDeployHook,
66
95
  )
67
96
  from snowflake.cli.api.project.schemas.updatable_model import (
68
97
  DiscriminatorField,
69
98
  IdentifierField,
99
+ UpdatableModel,
70
100
  )
71
101
  from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
72
- from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
73
102
  from snowflake.cli.api.project.util import (
74
103
  SCHEMA_AND_NAME,
104
+ VALID_IDENTIFIER_REGEX,
75
105
  append_test_resource_suffix,
76
- extract_schema,
77
106
  identifier_to_show_like_pattern,
107
+ same_identifiers,
108
+ sql_match,
78
109
  to_identifier,
79
110
  unquote_identifier,
80
111
  )
@@ -82,30 +113,64 @@ from snowflake.cli.api.utils.cursor import find_all_rows
82
113
  from snowflake.connector import DictCursor, ProgrammingError
83
114
  from snowflake.connector.cursor import SnowflakeCursor
84
115
 
116
+ ApplicationPackageChildrenTypes = (
117
+ StreamlitEntityModel | FunctionEntityModel | ProcedureEntityModel
118
+ )
85
119
 
86
- class ApplicationPackageEntityModel(EntityModelBase):
87
- type: Literal["application package"] = DiscriminatorField() # noqa: A003
88
- artifacts: List[Union[PathMapping, str]] = Field(
89
- title="List of paths or file source/destination pairs to add to the deploy root",
120
+
121
+ class ApplicationPackageChildIdentifier(UpdatableModel):
122
+ schema_: Optional[str] = Field(
123
+ title="Child entity schema", alias="schema", default=None
124
+ )
125
+
126
+
127
+ class EnsureUsableByField(UpdatableModel):
128
+ application_roles: Optional[Union[str, Set[str]]] = Field(
129
+ title="One or more application roles to be granted with the required privileges",
130
+ default=None,
90
131
  )
132
+
133
+ @field_validator("application_roles")
134
+ @classmethod
135
+ def ensure_app_roles_is_a_set(
136
+ cls, application_roles: Optional[Union[str, Set[str]]]
137
+ ) -> Optional[Union[Set[str]]]:
138
+ if isinstance(application_roles, str):
139
+ return set([application_roles])
140
+ return application_roles
141
+
142
+
143
+ class ApplicationPackageChildField(UpdatableModel):
144
+ target: str = Field(title="The key of the entity to include in this package")
145
+ ensure_usable_by: Optional[EnsureUsableByField] = Field(
146
+ title="Automatically grant the required privileges on the child object and its schema",
147
+ default=None,
148
+ )
149
+ identifier: ApplicationPackageChildIdentifier = Field(
150
+ title="Entity identifier", default=None
151
+ )
152
+
153
+
154
+ class ApplicationPackageEntityModel(EntityModelBaseWithArtifacts):
155
+ type: Literal["application package"] = DiscriminatorField() # noqa: A003
91
156
  bundle_root: Optional[str] = Field(
92
- title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored.",
157
+ title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored",
93
158
  default="output/bundle/",
94
159
  )
95
- deploy_root: Optional[str] = Field(
96
- title="Folder at the root of your project where the build step copies the artifacts",
97
- default="output/deploy/",
160
+ children_artifacts_dir: Optional[str] = Field(
161
+ title="Folder under deploy_root where the child artifacts will be stored",
162
+ default="_children/",
98
163
  )
99
164
  generated_root: Optional[str] = Field(
100
- title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written.",
165
+ title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written",
101
166
  default="__generated/",
102
167
  )
103
168
  stage: Optional[str] = IdentifierField(
104
- title="Identifier of the stage that stores the application artifacts.",
169
+ title="Identifier of the stage that stores the application artifacts",
105
170
  default="app_src.stage",
106
171
  )
107
172
  scratch_stage: Optional[str] = IdentifierField(
108
- title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI.",
173
+ title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI",
109
174
  default="app_src.stage_snowflake_cli_scratch",
110
175
  )
111
176
  distribution: Optional[DistributionOptions] = Field(
@@ -117,6 +182,24 @@ class ApplicationPackageEntityModel(EntityModelBase):
117
182
  default="",
118
183
  )
119
184
 
185
+ stage_subdirectory: Optional[str] = Field(
186
+ title="Subfolder in stage to upload the artifacts to, instead of the root of the application package's stage",
187
+ default="",
188
+ )
189
+ children: Optional[List[ApplicationPackageChildField]] = Field(
190
+ title="Entities that will be bundled and deployed as part of this application package",
191
+ default=[],
192
+ )
193
+
194
+ @field_validator("children")
195
+ @classmethod
196
+ def verify_children_behind_flag(
197
+ cls, input_value: Optional[List[ApplicationPackageChildField]]
198
+ ) -> Optional[List[ApplicationPackageChildField]]:
199
+ if input_value and not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
200
+ raise AttributeError("Application package children are not supported yet")
201
+ return input_value
202
+
120
203
  @field_validator("identifier")
121
204
  @classmethod
122
205
  def append_test_resource_suffix_to_identifier(
@@ -130,23 +213,6 @@ class ApplicationPackageEntityModel(EntityModelBase):
130
213
  return input_value.model_copy(update=dict(name=with_suffix))
131
214
  return with_suffix
132
215
 
133
- @field_validator("artifacts")
134
- @classmethod
135
- def transform_artifacts(
136
- cls, orig_artifacts: List[Union[PathMapping, str]]
137
- ) -> List[PathMapping]:
138
- transformed_artifacts = []
139
- if orig_artifacts is None:
140
- return transformed_artifacts
141
-
142
- for artifact in orig_artifacts:
143
- if isinstance(artifact, PathMapping):
144
- transformed_artifacts.append(artifact)
145
- else:
146
- transformed_artifacts.append(PathMapping(src=artifact))
147
-
148
- return transformed_artifacts
149
-
150
216
  @field_validator("stage")
151
217
  @classmethod
152
218
  def validate_source_stage(cls, input_value: str):
@@ -157,6 +223,7 @@ class ApplicationPackageEntityModel(EntityModelBase):
157
223
  return input_value
158
224
 
159
225
 
226
+ @attach_spans_to_entity_actions(entity_name="app_pkg")
160
227
  class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
161
228
  """
162
229
  A Native App application package.
@@ -168,7 +235,15 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
168
235
 
169
236
  @property
170
237
  def deploy_root(self) -> Path:
171
- return self.project_root / self._entity_model.deploy_root
238
+ return (
239
+ self.project_root
240
+ / self._entity_model.deploy_root
241
+ / self._entity_model.stage_subdirectory
242
+ )
243
+
244
+ @property
245
+ def children_artifacts_deploy_root(self) -> Path:
246
+ return self.deploy_root / self._entity_model.children_artifacts_dir
172
247
 
173
248
  @property
174
249
  def bundle_root(self) -> Path:
@@ -195,12 +270,16 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
195
270
  ) or to_identifier(self._workspace_ctx.default_warehouse)
196
271
 
197
272
  @property
198
- def stage_fqn(self) -> str:
199
- return f"{self.name}.{self._entity_model.stage}"
273
+ def scratch_stage_path(self) -> DefaultStagePathParts:
274
+ return DefaultStagePathParts.from_fqn(
275
+ f"{self.name}.{self._entity_model.scratch_stage}"
276
+ )
200
277
 
201
- @property
202
- def scratch_stage_fqn(self) -> str:
203
- return f"{self.name}.{self._entity_model.scratch_stage}"
278
+ @cached_property
279
+ def stage_path(self) -> DefaultStagePathParts:
280
+ stage_fqn = f"{self.name}.{self._entity_model.stage}"
281
+ subdir = self._entity_model.stage_subdirectory
282
+ return DefaultStagePathParts.from_fqn(stage_fqn, subdir)
204
283
 
205
284
  @property
206
285
  def post_deploy_hooks(self) -> list[PostDeployHook] | None:
@@ -208,7 +287,24 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
208
287
  return model.meta and model.meta.post_deploy
209
288
 
210
289
  def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
211
- return self._bundle()
290
+ return self._bundle(action_ctx)
291
+
292
+ def action_diff(
293
+ self, action_ctx: ActionContext, print_to_console: bool, *args, **kwargs
294
+ ):
295
+ """
296
+ Compute the diff between the local artifacts and the remote ones on the stage.
297
+ """
298
+ bundle_map = self._bundle()
299
+ diff = compute_stage_diff(
300
+ local_root=self.deploy_root,
301
+ stage_path=self.stage_path,
302
+ )
303
+
304
+ if print_to_console:
305
+ print_diff_to_console(diff, bundle_map)
306
+
307
+ return diff
212
308
 
213
309
  def action_deploy(
214
310
  self,
@@ -219,18 +315,18 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
219
315
  validate: bool,
220
316
  interactive: bool,
221
317
  force: bool,
222
- stage_fqn: Optional[str] = None,
223
318
  *args,
224
319
  **kwargs,
225
320
  ):
226
321
  return self._deploy(
322
+ action_ctx=action_ctx,
227
323
  bundle_map=None,
228
324
  prune=prune,
229
325
  recursive=recursive,
230
326
  paths=paths,
231
327
  print_diff=True,
232
328
  validate=validate,
233
- stage_fqn=stage_fqn or self.stage_fqn,
329
+ stage_path=self.stage_path,
234
330
  interactive=interactive,
235
331
  force=force,
236
332
  )
@@ -248,21 +344,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
248
344
  )
249
345
  return
250
346
 
251
- with sql_executor.use_role(self.role):
252
- # 2. Check for versions in the application package
253
- show_versions_query = f"show versions in application package {self.name}"
254
- show_versions_cursor = sql_executor.execute_query(
255
- show_versions_query, cursor_class=DictCursor
256
- )
257
- if show_versions_cursor.rowcount is None:
258
- raise SnowflakeSQLExecutionError(show_versions_query)
259
-
260
- if show_versions_cursor.rowcount > 0:
261
- # allow dropping a package with versions when --force is set
262
- if not force_drop:
263
- raise CouldNotDropApplicationPackageWithVersions(
264
- "Drop versions first, or use --force to override."
265
- )
347
+ # 2. Check for versions in the application package
348
+ versions_in_pkg = get_snowflake_facade().show_versions(self.name, self.role)
349
+ if len(versions_in_pkg) > 0:
350
+ # allow dropping a package with versions when --force is set
351
+ if not force_drop:
352
+ raise CouldNotDropApplicationPackageWithVersions(
353
+ "Drop versions first, or use --force to override."
354
+ )
266
355
 
267
356
  # 3. Check distribution of the existing application package
268
357
  actual_distribution = self.get_app_pkg_distribution_in_snowflake()
@@ -323,6 +412,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
323
412
  **kwargs,
324
413
  ):
325
414
  self.validate_setup_script(
415
+ action_ctx=action_ctx,
326
416
  use_scratch_stage=use_scratch_stage,
327
417
  interactive=interactive,
328
418
  force=force,
@@ -336,15 +426,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
336
426
  Get all existing versions, if defined, for an application package.
337
427
  It executes a 'show versions in application package' query and returns all the results.
338
428
  """
339
- sql_executor = get_sql_executor()
340
- with sql_executor.use_role(self.role):
341
- show_obj_query = f"show versions in application package {self.name}"
342
- show_obj_cursor = sql_executor.execute_query(show_obj_query)
343
-
344
- if show_obj_cursor.rowcount is None:
345
- raise SnowflakeSQLExecutionError(show_obj_query)
346
-
347
- return show_obj_cursor
429
+ return get_snowflake_facade().show_versions(self.name, self.role)
348
430
 
349
431
  def action_version_create(
350
432
  self,
@@ -355,9 +437,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
355
437
  skip_git_check: bool,
356
438
  interactive: bool,
357
439
  force: bool,
440
+ from_stage: Optional[bool],
358
441
  *args,
359
442
  **kwargs,
360
- ):
443
+ ) -> VersionInfo:
361
444
  """
362
445
  Create a version and/or patch for a new or existing application package.
363
446
  Always performs a deploy action before creating version or patch.
@@ -372,12 +455,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
372
455
  else:
373
456
  policy = DenyAlwaysPolicy()
374
457
 
375
- if skip_git_check:
376
- git_policy = DenyAlwaysPolicy()
377
- else:
378
- git_policy = AllowAlwaysPolicy()
379
-
380
- bundle_map = self._bundle()
458
+ bundle_map = self._bundle(action_ctx)
381
459
  resolved_version, resolved_patch, resolved_label = self.resolve_version_info(
382
460
  version=version,
383
461
  patch=patch,
@@ -387,20 +465,31 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
387
465
  interactive=interactive,
388
466
  )
389
467
 
390
- if git_policy.should_proceed():
468
+ if not skip_git_check:
391
469
  self.check_index_changes_in_git_repo(policy=policy, interactive=interactive)
392
470
 
393
- self._deploy(
394
- bundle_map=bundle_map,
395
- prune=True,
396
- recursive=True,
397
- paths=[],
398
- print_diff=True,
399
- validate=True,
400
- stage_fqn=self.stage_fqn,
401
- interactive=interactive,
402
- force=force,
403
- )
471
+ # if user is asking to create the version from the current stage,
472
+ # then do not re-deploy the artifacts or touch the stage
473
+ if from_stage:
474
+ # verify package exists:
475
+ if not self.get_existing_app_pkg_info():
476
+ raise ClickException(
477
+ "Cannot create version from stage because the application package does not exist yet. "
478
+ "Try removing --from-stage flag or executing `snow app deploy` to deploy the application package first."
479
+ )
480
+ else:
481
+ self._deploy(
482
+ action_ctx=action_ctx,
483
+ bundle_map=bundle_map,
484
+ prune=True,
485
+ recursive=True,
486
+ paths=[],
487
+ print_diff=True,
488
+ validate=True,
489
+ stage_path=self.stage_path,
490
+ interactive=interactive,
491
+ force=force,
492
+ )
404
493
 
405
494
  # Warn if the version exists in a release directive(s)
406
495
  try:
@@ -440,12 +529,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
440
529
  # Define a new version in the application package
441
530
  if not self.get_existing_version_info(resolved_version):
442
531
  self.add_new_version(version=resolved_version, label=resolved_label)
443
- return # A new version created automatically has patch 0, we do not need to further increment the patch.
532
+ # A new version created automatically has patch 0, we do not need to further increment the patch.
533
+ return VersionInfo(resolved_version, 0, resolved_label)
444
534
 
445
535
  # Add a new patch to an existing (old) version
446
- self.add_new_patch_to_version(
536
+ patch = self.add_new_patch_to_version(
447
537
  version=resolved_version, patch=resolved_patch, label=resolved_label
448
538
  )
539
+ return VersionInfo(resolved_version, patch, resolved_label)
449
540
 
450
541
  def action_version_drop(
451
542
  self,
@@ -492,7 +583,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
492
583
  """
493
584
  )
494
585
  )
495
- self._bundle()
586
+ self._bundle(action_ctx)
496
587
  version_info = find_version_info_in_manifest_file(self.deploy_root)
497
588
  version = version_info.version_name
498
589
  if not version:
@@ -524,20 +615,283 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
524
615
  raise typer.Exit(1)
525
616
 
526
617
  # Drop the version
527
- sql_executor = get_sql_executor()
528
- with sql_executor.use_role(self.role):
529
- try:
530
- sql_executor.execute_query(
531
- f"alter application package {self.name} drop version {version}"
532
- )
533
- except ProgrammingError as err:
534
- raise err # e.g. version is referenced in a release directive(s)
618
+ get_snowflake_facade().drop_version_from_package(
619
+ package_name=self.name, version=version, role=self.role
620
+ )
535
621
 
536
622
  console.message(
537
623
  f"Version {version} in application package {self.name} dropped successfully."
538
624
  )
539
625
 
540
- def _bundle(self):
626
+ def _validate_target_accounts(self, accounts: list[str]) -> None:
627
+ """
628
+ Validates the target accounts provided by the user.
629
+ """
630
+ for account in accounts:
631
+ if not re.fullmatch(
632
+ f"{VALID_IDENTIFIER_REGEX}\\.{VALID_IDENTIFIER_REGEX}", account
633
+ ):
634
+ raise ClickException(
635
+ f"Target account {account} is not in a valid format. Make sure you provide the target account in the format 'org.account'."
636
+ )
637
+
638
+ def get_sanitized_release_channel(
639
+ self,
640
+ release_channel: Optional[str],
641
+ available_release_channels: Optional[list[ReleaseChannel]] = None,
642
+ ) -> Optional[str]:
643
+ """
644
+ Sanitize the release channel name provided by the user and validate it against the available release channels.
645
+
646
+ A return value of None indicates that release channels should not be used. Returns None if:
647
+ - Release channel is not provided
648
+ - Release channels are not enabled in the application package and the user provided the default release channel
649
+ """
650
+ if not release_channel:
651
+ return None
652
+
653
+ if available_release_channels is None:
654
+ available_release_channels = get_snowflake_facade().show_release_channels(
655
+ self.name, self.role
656
+ )
657
+
658
+ if not available_release_channels and same_identifiers(
659
+ release_channel, DEFAULT_CHANNEL
660
+ ):
661
+ return None
662
+
663
+ self.validate_release_channel(release_channel, available_release_channels)
664
+ return release_channel
665
+
666
+ def validate_release_channel(
667
+ self,
668
+ release_channel: str,
669
+ available_release_channels: Optional[list[ReleaseChannel]] = None,
670
+ ) -> None:
671
+ """
672
+ Validates the release channel provided by the user and make sure it is a valid release channel for the application package.
673
+ """
674
+
675
+ if available_release_channels is None:
676
+ available_release_channels = get_snowflake_facade().show_release_channels(
677
+ self.name, self.role
678
+ )
679
+ if not available_release_channels:
680
+ raise UsageError(
681
+ f"Release channels are not enabled for application package {self.name}."
682
+ )
683
+ for channel in available_release_channels:
684
+ if unquote_identifier(release_channel) == channel["name"]:
685
+ return
686
+
687
+ raise UsageError(
688
+ f"Release channel {release_channel} is not available in application package {self.name}. "
689
+ f"Available release channels are: ({', '.join(channel['name'] for channel in available_release_channels)})."
690
+ )
691
+
692
+ def action_release_directive_list(
693
+ self,
694
+ action_ctx: ActionContext,
695
+ release_channel: Optional[str],
696
+ like: str,
697
+ *args,
698
+ **kwargs,
699
+ ) -> list[dict[str, Any]]:
700
+ """
701
+ Get all existing release directives for an application package.
702
+ Limit the results to a specific release channel, if provided.
703
+
704
+ If `like` is provided, only release directives matching the SQL LIKE pattern are listed.
705
+ """
706
+ release_channel = self.get_sanitized_release_channel(release_channel)
707
+
708
+ release_directives = get_snowflake_facade().show_release_directives(
709
+ package_name=self.name,
710
+ role=self.role,
711
+ release_channel=release_channel,
712
+ )
713
+
714
+ return [
715
+ directive
716
+ for directive in release_directives
717
+ if sql_match(pattern=like, value=directive.get("name", ""))
718
+ ]
719
+
720
+ def action_release_directive_set(
721
+ self,
722
+ action_ctx: ActionContext,
723
+ version: str,
724
+ patch: int,
725
+ release_directive: str,
726
+ release_channel: str,
727
+ target_accounts: Optional[list[str]],
728
+ *args,
729
+ **kwargs,
730
+ ):
731
+ """
732
+ Sets a release directive to the specified version and patch using the specified release channel.
733
+ Target accounts can only be specified for non-default release directives.
734
+
735
+ For non-default release directives, update the existing release directive if target accounts are not provided.
736
+ """
737
+ if target_accounts:
738
+ self._validate_target_accounts(target_accounts)
739
+
740
+ if target_accounts and same_identifiers(release_directive, DEFAULT_DIRECTIVE):
741
+ raise BadOptionUsage(
742
+ "target_accounts",
743
+ "Target accounts can only be specified for non-default named release directives.",
744
+ )
745
+
746
+ sanitized_release_channel = self.get_sanitized_release_channel(release_channel)
747
+
748
+ get_snowflake_facade().set_release_directive(
749
+ package_name=self.name,
750
+ release_directive=release_directive,
751
+ release_channel=sanitized_release_channel,
752
+ target_accounts=target_accounts,
753
+ version=version,
754
+ patch=patch,
755
+ role=self.role,
756
+ )
757
+
758
+ def action_release_directive_unset(
759
+ self,
760
+ action_ctx: ActionContext,
761
+ release_directive: str,
762
+ release_channel: str,
763
+ ):
764
+ """
765
+ Unsets a release directive from the specified release channel.
766
+ """
767
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
768
+ raise ClickException(
769
+ "Cannot unset default release directive. Please specify a non-default release directive."
770
+ )
771
+
772
+ get_snowflake_facade().unset_release_directive(
773
+ package_name=self.name,
774
+ release_directive=release_directive,
775
+ release_channel=self.get_sanitized_release_channel(release_channel),
776
+ role=self.role,
777
+ )
778
+
779
+ def action_release_directive_add_accounts(
780
+ self,
781
+ action_ctx: ActionContext,
782
+ release_directive: str,
783
+ release_channel: str,
784
+ target_accounts: list[str],
785
+ *args,
786
+ **kwargs,
787
+ ):
788
+ """
789
+ Adds target accounts to a release directive.
790
+ """
791
+
792
+ if not target_accounts:
793
+ raise ClickException("No target accounts provided.")
794
+
795
+ self._validate_target_accounts(target_accounts)
796
+
797
+ get_snowflake_facade().add_accounts_to_release_directive(
798
+ package_name=self.name,
799
+ release_directive=release_directive,
800
+ release_channel=self.get_sanitized_release_channel(release_channel),
801
+ target_accounts=target_accounts,
802
+ role=self.role,
803
+ )
804
+
805
+ def action_release_directive_remove_accounts(
806
+ self,
807
+ action_ctx: ActionContext,
808
+ release_directive: str,
809
+ release_channel: str,
810
+ target_accounts: list[str],
811
+ *args,
812
+ **kwargs,
813
+ ):
814
+ """
815
+ Removes target accounts from a release directive.
816
+ """
817
+
818
+ if not target_accounts:
819
+ raise ClickException("No target accounts provided.")
820
+
821
+ self._validate_target_accounts(target_accounts)
822
+
823
+ get_snowflake_facade().remove_accounts_from_release_directive(
824
+ package_name=self.name,
825
+ release_directive=release_directive,
826
+ release_channel=self.get_sanitized_release_channel(release_channel),
827
+ target_accounts=target_accounts,
828
+ role=self.role,
829
+ )
830
+
831
+ def _print_channel_to_console(self, channel: ReleaseChannel) -> None:
832
+ """
833
+ Prints the release channel details to the console.
834
+ """
835
+ console = self._workspace_ctx.console
836
+
837
+ console.message(f"""[bold]{channel["name"]}[/bold]""")
838
+ accounts_list: Optional[list[str]] = channel["targets"].get("accounts")
839
+ target_accounts = (
840
+ f"({', '.join(accounts_list)})"
841
+ if accounts_list is not None
842
+ else "ALL ACCOUNTS"
843
+ )
844
+
845
+ formatted_created_on = (
846
+ channel["created_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
847
+ if channel["created_on"]
848
+ else ""
849
+ )
850
+
851
+ formatted_updated_on = (
852
+ channel["updated_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
853
+ if channel["updated_on"]
854
+ else ""
855
+ )
856
+ with console.indented():
857
+ console.message(f"Description: {channel['description']}")
858
+ console.message(f"Versions: ({', '.join(channel['versions'])})")
859
+ console.message(f"Created on: {formatted_created_on}")
860
+ console.message(f"Updated on: {formatted_updated_on}")
861
+ console.message(f"Target accounts: {target_accounts}")
862
+
863
+ def action_release_channel_list(
864
+ self,
865
+ action_ctx: ActionContext,
866
+ release_channel: Optional[str],
867
+ *args,
868
+ **kwargs,
869
+ ) -> list[ReleaseChannel]:
870
+ """
871
+ Get all existing release channels for an application package.
872
+ If `release_channel` is provided, only the specified release channel is listed.
873
+ """
874
+ console = self._workspace_ctx.console
875
+ available_channels = get_snowflake_facade().show_release_channels(
876
+ self.name, self.role
877
+ )
878
+
879
+ filtered_channels = [
880
+ channel
881
+ for channel in available_channels
882
+ if release_channel is None
883
+ or unquote_identifier(release_channel) == channel["name"]
884
+ ]
885
+
886
+ if not filtered_channels:
887
+ console.message("No release channels found.")
888
+ else:
889
+ for channel in filtered_channels:
890
+ self._print_channel_to_console(channel)
891
+
892
+ return filtered_channels
893
+
894
+ def _bundle(self, action_ctx: ActionContext = None):
541
895
  model = self._entity_model
542
896
  bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts)
543
897
  bundle_context = BundleContext(
@@ -550,17 +904,391 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
550
904
  )
551
905
  compiler = NativeAppCompiler(bundle_context)
552
906
  compiler.compile_artifacts()
907
+
908
+ if self._entity_model.children:
909
+ # Bundle children and append their SQL to setup script
910
+ # TODO Consider re-writing the logic below as a processor
911
+ children_sql = self._bundle_children(action_ctx=action_ctx)
912
+ setup_file_path = find_setup_script_file(deploy_root=self.deploy_root)
913
+ with open(setup_file_path, "r", encoding="utf-8") as file:
914
+ existing_setup_script = file.read()
915
+ if setup_file_path.is_symlink():
916
+ setup_file_path.unlink()
917
+ with open(setup_file_path, "w", encoding="utf-8") as file:
918
+ file.write(existing_setup_script)
919
+ file.write("\n-- AUTO GENERATED CHILDREN SECTION\n")
920
+ file.write("\n".join(children_sql))
921
+ file.write("\n")
922
+
553
923
  return bundle_map
554
924
 
925
+ def action_release_channel_add_accounts(
926
+ self,
927
+ action_ctx: ActionContext,
928
+ release_channel: str,
929
+ target_accounts: list[str],
930
+ *args,
931
+ **kwargs,
932
+ ):
933
+ """
934
+ Adds target accounts to a release channel.
935
+ """
936
+
937
+ if not target_accounts:
938
+ raise ClickException("No target accounts provided.")
939
+
940
+ self.validate_release_channel(release_channel)
941
+ self._validate_target_accounts(target_accounts)
942
+
943
+ get_snowflake_facade().add_accounts_to_release_channel(
944
+ package_name=self.name,
945
+ release_channel=release_channel,
946
+ target_accounts=target_accounts,
947
+ role=self.role,
948
+ )
949
+
950
+ def action_release_channel_remove_accounts(
951
+ self,
952
+ action_ctx: ActionContext,
953
+ release_channel: str,
954
+ target_accounts: list[str],
955
+ *args,
956
+ **kwargs,
957
+ ):
958
+ """
959
+ Removes target accounts from a release channel.
960
+ """
961
+
962
+ if not target_accounts:
963
+ raise ClickException("No target accounts provided.")
964
+
965
+ self.validate_release_channel(release_channel)
966
+ self._validate_target_accounts(target_accounts)
967
+
968
+ get_snowflake_facade().remove_accounts_from_release_channel(
969
+ package_name=self.name,
970
+ release_channel=release_channel,
971
+ target_accounts=target_accounts,
972
+ role=self.role,
973
+ )
974
+
975
+ def action_release_channel_set_accounts(
976
+ self,
977
+ action_ctx: ActionContext,
978
+ release_channel: str,
979
+ target_accounts: list[str],
980
+ *args,
981
+ **kwargs,
982
+ ):
983
+ """
984
+ Sets target accounts for a release channel.
985
+ """
986
+
987
+ if not target_accounts:
988
+ raise ClickException("No target accounts provided.")
989
+
990
+ self.validate_release_channel(release_channel)
991
+ self._validate_target_accounts(target_accounts)
992
+
993
+ get_snowflake_facade().set_accounts_for_release_channel(
994
+ package_name=self.name,
995
+ release_channel=release_channel,
996
+ target_accounts=target_accounts,
997
+ role=self.role,
998
+ )
999
+
1000
+ def action_release_channel_add_version(
1001
+ self,
1002
+ action_ctx: ActionContext,
1003
+ release_channel: str,
1004
+ version: str,
1005
+ *args,
1006
+ **kwargs,
1007
+ ):
1008
+ """
1009
+ Adds a version to a release channel.
1010
+ """
1011
+
1012
+ self.validate_release_channel(release_channel)
1013
+ get_snowflake_facade().add_version_to_release_channel(
1014
+ package_name=self.name,
1015
+ release_channel=release_channel,
1016
+ version=version,
1017
+ role=self.role,
1018
+ )
1019
+
1020
+ def action_release_channel_remove_version(
1021
+ self,
1022
+ action_ctx: ActionContext,
1023
+ release_channel: str,
1024
+ version: str,
1025
+ *args,
1026
+ **kwargs,
1027
+ ):
1028
+ """
1029
+ Removes a version from a release channel.
1030
+ """
1031
+
1032
+ self.validate_release_channel(release_channel)
1033
+ get_snowflake_facade().remove_version_from_release_channel(
1034
+ package_name=self.name,
1035
+ release_channel=release_channel,
1036
+ version=version,
1037
+ role=self.role,
1038
+ )
1039
+
1040
+ def _find_version_with_no_recent_update(
1041
+ self, versions_info: list[Version], free_versions: set[str]
1042
+ ) -> Optional[str]:
1043
+ """
1044
+ Finds the version with the oldest created_on date from the free versions.
1045
+ """
1046
+
1047
+ if not free_versions:
1048
+ return None
1049
+
1050
+ # map of versionId to last Updated Date. Last Updated Date is based on patch creation date.
1051
+ last_updated_map: dict[str, datetime] = {}
1052
+ for version_info in versions_info:
1053
+ last_updated_value = last_updated_map.get(version_info["version"], None)
1054
+ if (
1055
+ not last_updated_value
1056
+ or version_info["created_on"] > last_updated_value
1057
+ ):
1058
+ last_updated_map[version_info["version"]] = version_info["created_on"]
1059
+
1060
+ oldest_version = None
1061
+ oldest_version_last_updated_on = None
1062
+
1063
+ for version in free_versions:
1064
+ last_updated = last_updated_map[version]
1065
+ if not oldest_version or last_updated < oldest_version_last_updated_on:
1066
+ oldest_version = version
1067
+ oldest_version_last_updated_on = last_updated
1068
+
1069
+ return oldest_version
1070
+
1071
+ def action_publish(
1072
+ self,
1073
+ action_ctx: ActionContext,
1074
+ version: Optional[str],
1075
+ patch: Optional[int],
1076
+ release_channel: Optional[str],
1077
+ release_directive: str,
1078
+ interactive: bool,
1079
+ force: bool,
1080
+ *args,
1081
+ create_version: bool = False,
1082
+ from_stage: bool = False,
1083
+ label: Optional[str] = None,
1084
+ **kwargs,
1085
+ ) -> VersionInfo:
1086
+ """
1087
+ Publishes a version and a patch to a release directive of a release channel.
1088
+
1089
+ The version is first added to the release channel,
1090
+ and then the release directive is set to the version and patch provided.
1091
+
1092
+ If the number of versions in a release channel exceeds the maximum allowable versions,
1093
+ the user is prompted to remove an existing version to make space for the new version.
1094
+ """
1095
+ if force:
1096
+ policy = AllowAlwaysPolicy()
1097
+ elif interactive:
1098
+ policy = AskAlwaysPolicy()
1099
+ else:
1100
+ policy = DenyAlwaysPolicy()
1101
+
1102
+ if from_stage and not create_version:
1103
+ raise UsageError(
1104
+ "--from-stage flag can only be used with --create-version flag."
1105
+ )
1106
+ if label is not None and not create_version:
1107
+ raise UsageError("--label can only be used with --create-version flag.")
1108
+
1109
+ console = self._workspace_ctx.console
1110
+ if create_version:
1111
+ result = self.action_version_create(
1112
+ action_ctx=action_ctx,
1113
+ version=version,
1114
+ patch=patch,
1115
+ label=label,
1116
+ skip_git_check=True,
1117
+ interactive=interactive,
1118
+ force=force,
1119
+ from_stage=from_stage,
1120
+ )
1121
+ version = result.version_name
1122
+ patch = result.patch_number
1123
+
1124
+ if version is None:
1125
+ raise UsageError(
1126
+ "Please provide a version using --version or use --create-version flag to create a version based on the manifest file."
1127
+ )
1128
+ if patch is None:
1129
+ raise UsageError(
1130
+ "Please provide a patch number using --patch or use --create-version flag to auto create a patch."
1131
+ )
1132
+
1133
+ versions_info = get_snowflake_facade().show_versions(self.name, self.role)
1134
+
1135
+ available_patches = [
1136
+ version_info["patch"]
1137
+ for version_info in versions_info
1138
+ if version_info["version"] == unquote_identifier(version)
1139
+ ]
1140
+
1141
+ if not available_patches:
1142
+ raise ClickException(
1143
+ f"Version {version} does not exist in application package {self.name}. Use --create-version flag to create a new version."
1144
+ )
1145
+
1146
+ if patch not in available_patches:
1147
+ raise ClickException(
1148
+ f"Patch {patch} does not exist for version {version} in application package {self.name}. Use --create-version flag to add a new patch."
1149
+ )
1150
+
1151
+ available_release_channels = get_snowflake_facade().show_release_channels(
1152
+ self.name, self.role
1153
+ )
1154
+
1155
+ release_channel = self.get_sanitized_release_channel(
1156
+ release_channel, available_release_channels
1157
+ )
1158
+
1159
+ if release_channel:
1160
+ release_channel_info = {}
1161
+ for channel_info in available_release_channels:
1162
+ if channel_info["name"] == unquote_identifier(release_channel):
1163
+ release_channel_info = channel_info
1164
+ break
1165
+
1166
+ versions_in_channel = release_channel_info["versions"]
1167
+ if unquote_identifier(version) not in release_channel_info["versions"]:
1168
+ if len(versions_in_channel) >= MAX_VERSIONS_IN_RELEASE_CHANNEL:
1169
+ # If we hit the maximum allowable versions in a release channel, we need to remove one version to make space for the new version
1170
+ all_release_directives = (
1171
+ get_snowflake_facade().show_release_directives(
1172
+ package_name=self.name,
1173
+ role=self.role,
1174
+ release_channel=release_channel,
1175
+ )
1176
+ )
1177
+
1178
+ # check which versions are attached to any release directive
1179
+ targeted_versions = {d["version"] for d in all_release_directives}
1180
+
1181
+ free_versions = {
1182
+ v for v in versions_in_channel if v not in targeted_versions
1183
+ }
1184
+
1185
+ if not free_versions:
1186
+ raise ClickException(
1187
+ f"Maximum number of versions in release channel {release_channel} reached. Cannot add more versions."
1188
+ )
1189
+
1190
+ version_to_remove = self._find_version_with_no_recent_update(
1191
+ versions_info, free_versions
1192
+ )
1193
+ user_prompt = f"Maximum number of versions in release channel reached. Would you like to remove version {version_to_remove} to make space for version {version}?"
1194
+ if not policy.should_proceed(user_prompt):
1195
+ raise ClickException(
1196
+ "Cannot proceed with publishing the new version. Please remove an existing version from the release channel to make space for the new version, or use --force to automatically clean up unused versions."
1197
+ )
1198
+
1199
+ console.warning(
1200
+ f"Maximum number of versions in release channel reached. Removing version {version_to_remove} from release_channel {release_channel} to make space for version {version}."
1201
+ )
1202
+ get_snowflake_facade().remove_version_from_release_channel(
1203
+ package_name=self.name,
1204
+ release_channel=release_channel,
1205
+ version=version_to_remove,
1206
+ role=self.role,
1207
+ )
1208
+
1209
+ get_snowflake_facade().add_version_to_release_channel(
1210
+ package_name=self.name,
1211
+ release_channel=release_channel,
1212
+ version=version,
1213
+ role=self.role,
1214
+ )
1215
+
1216
+ get_snowflake_facade().set_release_directive(
1217
+ package_name=self.name,
1218
+ release_directive=release_directive,
1219
+ release_channel=release_channel,
1220
+ target_accounts=None,
1221
+ version=version,
1222
+ patch=patch,
1223
+ role=self.role,
1224
+ )
1225
+ return VersionInfo(version, patch, None)
1226
+
1227
+ def _bundle_children(self, action_ctx: ActionContext) -> List[str]:
1228
+ # Create _children directory
1229
+ children_artifacts_dir = self.children_artifacts_deploy_root
1230
+ os.makedirs(children_artifacts_dir)
1231
+ children_sql = []
1232
+ for child in self._entity_model.children:
1233
+ # Create child sub directory
1234
+ child_artifacts_dir = children_artifacts_dir / sanitize_dir_name(
1235
+ child.target
1236
+ )
1237
+ try:
1238
+ os.makedirs(child_artifacts_dir)
1239
+ except FileExistsError:
1240
+ raise ClickException(
1241
+ f"Could not create sub-directory at {child_artifacts_dir}. Make sure child entity names do not collide with each other."
1242
+ )
1243
+ child_entity: ApplicationPackageChildInterface = action_ctx.get_entity(
1244
+ child.target
1245
+ )
1246
+ child_entity.bundle(child_artifacts_dir)
1247
+ app_role = (
1248
+ to_identifier(
1249
+ child.ensure_usable_by.application_roles.pop() # TODO Support more than one application role
1250
+ )
1251
+ if child.ensure_usable_by and child.ensure_usable_by.application_roles
1252
+ else None
1253
+ )
1254
+ child_schema = (
1255
+ to_identifier(child.identifier.schema_)
1256
+ if child.identifier and child.identifier.schema_
1257
+ else None
1258
+ )
1259
+ children_sql.append(
1260
+ child_entity.get_deploy_sql(
1261
+ artifacts_dir=child_artifacts_dir.relative_to(self.deploy_root),
1262
+ schema=child_schema,
1263
+ # TODO Allow users to override the hard-coded value for specific children
1264
+ replace=True,
1265
+ )
1266
+ )
1267
+ if app_role:
1268
+ children_sql.append(
1269
+ f"CREATE APPLICATION ROLE IF NOT EXISTS {app_role};"
1270
+ )
1271
+ if child_schema:
1272
+ children_sql.append(
1273
+ f"GRANT USAGE ON SCHEMA {child_schema} TO APPLICATION ROLE {app_role};"
1274
+ )
1275
+ children_sql.append(
1276
+ child_entity.get_usage_grant_sql(
1277
+ app_role=app_role, schema=child_schema
1278
+ )
1279
+ )
1280
+ return children_sql
1281
+
555
1282
  def _deploy(
556
1283
  self,
1284
+ action_ctx: ActionContext,
557
1285
  bundle_map: BundleMap | None,
558
1286
  prune: bool,
559
1287
  recursive: bool,
560
1288
  paths: list[Path],
561
1289
  print_diff: bool,
562
1290
  validate: bool,
563
- stage_fqn: str,
1291
+ stage_path: StagePathParts,
564
1292
  interactive: bool,
565
1293
  force: bool,
566
1294
  run_post_deploy_hooks: bool = True,
@@ -575,10 +1303,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
575
1303
  policy = DenyAlwaysPolicy()
576
1304
 
577
1305
  console = workspace_ctx.console
578
- stage_fqn = stage_fqn or self.stage_fqn
1306
+ stage_path = stage_path or self.stage_path
579
1307
 
580
1308
  # 1. Create a bundle if one wasn't passed in
581
- bundle_map = bundle_map or self._bundle()
1309
+ bundle_map = bundle_map or self._bundle(action_ctx)
582
1310
 
583
1311
  # 2. Create an empty application package, if none exists
584
1312
  try:
@@ -590,17 +1318,15 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
590
1318
 
591
1319
  with get_sql_executor().use_role(self.role):
592
1320
  # 3. Upload files from deploy root local folder to the above stage
593
- stage_schema = extract_schema(stage_fqn)
594
1321
  diff = sync_deploy_root_with_stage(
595
1322
  console=console,
596
1323
  deploy_root=self.deploy_root,
597
1324
  package_name=self.name,
598
- stage_schema=stage_schema,
599
1325
  bundle_map=bundle_map,
600
1326
  role=self.role,
601
1327
  prune=prune,
602
1328
  recursive=recursive,
603
- stage_fqn=stage_fqn,
1329
+ stage_path=stage_path,
604
1330
  local_paths_to_sync=paths,
605
1331
  print_diff=print_diff,
606
1332
  )
@@ -610,6 +1336,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
610
1336
 
611
1337
  if validate:
612
1338
  self.validate_setup_script(
1339
+ action_ctx=action_ctx,
613
1340
  use_scratch_stage=False,
614
1341
  interactive=interactive,
615
1342
  force=force,
@@ -657,7 +1384,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
657
1384
  It executes a 'show release directives in application package' query and returns the filtered results, if they exist.
658
1385
  """
659
1386
  release_directives = get_snowflake_facade().show_release_directives(
660
- self.name, self.role
1387
+ package_name=self.name, role=self.role
661
1388
  )
662
1389
  return [
663
1390
  directive
@@ -678,7 +1405,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
678
1405
  get_snowflake_facade().create_version_in_package(
679
1406
  role=self.role,
680
1407
  package_name=self.name,
681
- stage_fqn=self.stage_fqn,
1408
+ path_to_version_directory=self.stage_path.full_path,
682
1409
  version=version,
683
1410
  label=label,
684
1411
  )
@@ -688,9 +1415,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
688
1415
 
689
1416
  def add_new_patch_to_version(
690
1417
  self, version: str, patch: int | None = None, label: str | None = None
691
- ):
1418
+ ) -> int:
692
1419
  """
693
1420
  Add a new patch, optionally a custom one, to an existing version in an application package.
1421
+ Returns the patch number of the newly created patch.
694
1422
  """
695
1423
  console = self._workspace_ctx.console
696
1424
 
@@ -702,7 +1430,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
702
1430
  new_patch = get_snowflake_facade().add_patch_to_package_version(
703
1431
  role=self.role,
704
1432
  package_name=self.name,
705
- stage_fqn=self.stage_fqn,
1433
+ path_to_version_directory=self.stage_path.full_path,
706
1434
  version=version,
707
1435
  patch=patch,
708
1436
  label=label,
@@ -710,6 +1438,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
710
1438
  console.message(
711
1439
  f"Patch {new_patch}{with_label_prompt} created for version {version} defined in application package {self.name}."
712
1440
  )
1441
+ return new_patch
713
1442
 
714
1443
  def check_index_changes_in_git_repo(
715
1444
  self, policy: PolicyBase, interactive: bool
@@ -795,7 +1524,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
795
1524
  ) -> bool:
796
1525
  """
797
1526
  Returns true if the 'distribution' attribute of an existing application package in snowflake
798
- is the same as the the attribute specified in project definition file.
1527
+ is the same as the attribute specified in project definition file.
799
1528
  """
800
1529
  model = self._entity_model
801
1530
  workspace_ctx = self._workspace_ctx
@@ -818,6 +1547,28 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
818
1547
  return False
819
1548
  return True
820
1549
 
1550
+ def _get_enable_release_channels_flag(self) -> Optional[bool]:
1551
+ """
1552
+ Returns the requested value of enable_release_channels flag for the application package.
1553
+ It retrieves the value from the configuration file and checks that the feature is enabled in the account.
1554
+ If return value is None, it means do not explicitly set the flag.
1555
+ """
1556
+ feature_flag_from_config = FeatureFlag.ENABLE_RELEASE_CHANNELS.get_value()
1557
+ feature_enabled_in_account = (
1558
+ get_snowflake_facade().get_ui_parameter(
1559
+ UIParameter.NA_FEATURE_RELEASE_CHANNELS, "ENABLED"
1560
+ )
1561
+ == "ENABLED"
1562
+ )
1563
+
1564
+ if feature_flag_from_config is not None and not feature_enabled_in_account:
1565
+ self._workspace_ctx.console.warning(
1566
+ f"Ignoring feature flag {FeatureFlag.ENABLE_RELEASE_CHANNELS.name} because release channels are not enabled in the current account."
1567
+ )
1568
+ return None
1569
+
1570
+ return feature_flag_from_config
1571
+
821
1572
  def create_app_package(self) -> None:
822
1573
  """
823
1574
  Creates the application package with our up-to-date stage if none exists.
@@ -845,21 +1596,23 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
845
1596
  if row_comment not in ALLOWED_SPECIAL_COMMENTS:
846
1597
  raise ApplicationPackageAlreadyExistsError(self.name)
847
1598
 
1599
+ # 4. Update the application package with setting enable_release_channels if necessary
1600
+ get_snowflake_facade().alter_application_package_properties(
1601
+ package_name=self.name,
1602
+ enable_release_channels=self._get_enable_release_channels_flag(),
1603
+ role=self.role,
1604
+ )
1605
+
848
1606
  return
849
1607
 
850
1608
  # If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
851
- sql_executor = get_sql_executor()
852
- with sql_executor.use_role(self.role):
853
- console.step(f"Creating new application package {self.name} in account.")
854
- sql_executor.execute_query(
855
- dedent(
856
- f"""\
857
- create application package {self.name}
858
- comment = {SPECIAL_COMMENT}
859
- distribution = {model.distribution}
860
- """
861
- )
862
- )
1609
+ console.step(f"Creating new application package {self.name} in account.")
1610
+ get_snowflake_facade().create_application_package(
1611
+ role=self.role,
1612
+ enable_release_channels=self._get_enable_release_channels_flag(),
1613
+ distribution=model.distribution,
1614
+ package_name=self.name,
1615
+ )
863
1616
 
864
1617
  def execute_post_deploy_hooks(self):
865
1618
  execute_post_deploy_hooks(
@@ -873,7 +1626,11 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
873
1626
  )
874
1627
 
875
1628
  def validate_setup_script(
876
- self, use_scratch_stage: bool, interactive: bool, force: bool
1629
+ self,
1630
+ action_ctx: ActionContext,
1631
+ use_scratch_stage: bool,
1632
+ interactive: bool,
1633
+ force: bool,
877
1634
  ):
878
1635
  workspace_ctx = self._workspace_ctx
879
1636
  console = workspace_ctx.console
@@ -881,6 +1638,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
881
1638
  """Validates Native App setup script SQL."""
882
1639
  with console.phase(f"Validating Snowflake Native App setup script."):
883
1640
  validation_result = self.get_validation_result(
1641
+ action_ctx=action_ctx,
884
1642
  use_scratch_stage=use_scratch_stage,
885
1643
  force=force,
886
1644
  interactive=interactive,
@@ -900,26 +1658,35 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
900
1658
  if validation_result["status"] == "FAIL":
901
1659
  raise SetupScriptFailedValidation()
902
1660
 
1661
+ @span("validate_setup_script")
903
1662
  def get_validation_result(
904
- self, use_scratch_stage: bool, interactive: bool, force: bool
1663
+ self,
1664
+ action_ctx: ActionContext,
1665
+ use_scratch_stage: bool,
1666
+ interactive: bool,
1667
+ force: bool,
905
1668
  ):
906
1669
  """Call system$validate_native_app_setup() to validate deployed Native App setup script."""
907
- stage_fqn = self.stage_fqn
1670
+ stage_path = self.stage_path
908
1671
  if use_scratch_stage:
909
- stage_fqn = self.scratch_stage_fqn
1672
+ stage_path = self.scratch_stage_path
910
1673
  self._deploy(
1674
+ action_ctx=action_ctx,
911
1675
  bundle_map=None,
912
1676
  prune=True,
913
1677
  recursive=True,
914
1678
  paths=[],
915
1679
  print_diff=False,
916
1680
  validate=False,
917
- stage_fqn=self.scratch_stage_fqn,
1681
+ stage_path=stage_path,
918
1682
  interactive=interactive,
919
1683
  force=force,
920
1684
  run_post_deploy_hooks=False,
921
1685
  )
922
- prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
1686
+ prefixed_stage_fqn = StageManager.get_standard_stage_prefix(
1687
+ stage_path.full_path
1688
+ )
1689
+
923
1690
  sql_executor = get_sql_executor()
924
1691
  try:
925
1692
  cursor = sql_executor.execute_query(
@@ -936,11 +1703,11 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
936
1703
  finally:
937
1704
  if use_scratch_stage:
938
1705
  self._workspace_ctx.console.step(
939
- f"Dropping stage {self.scratch_stage_fqn}."
1706
+ f"Dropping stage {self.scratch_stage_path.stage}."
940
1707
  )
941
1708
  with sql_executor.use_role(self.role):
942
1709
  sql_executor.execute_query(
943
- f"drop stage if exists {self.scratch_stage_fqn}"
1710
+ f"drop stage if exists {self.scratch_stage_path.stage}"
944
1711
  )
945
1712
 
946
1713
  def resolve_version_info(
@@ -951,7 +1718,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
951
1718
  bundle_map: BundleMap | None,
952
1719
  policy: PolicyBase,
953
1720
  interactive: bool,
954
- ):
1721
+ ) -> VersionInfo:
955
1722
  """Determine version name, patch number, and label from CLI provided values and manifest.yml version entry.
956
1723
  @param [Optional] version: version name as specified in the command
957
1724
  @param [Optional] patch: patch number as specified in the command
@@ -959,12 +1726,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
959
1726
  @param [Optional] bundle_map: bundle_map if a deploy_root is prepared. _bundle() is performed otherwise.
960
1727
  @param policy: CLI policy
961
1728
  @param interactive: True if command is run in interactive mode, otherwise False
1729
+
1730
+ @return VersionInfo: version_name, patch_number, label resolved from CLI and manifest.yml
962
1731
  """
963
1732
  console = self._workspace_ctx.console
964
1733
 
965
1734
  resolved_version = None
966
1735
  resolved_patch = None
967
- resolved_label = ""
1736
+ resolved_label = None
968
1737
 
969
1738
  # If version is specified in CLI, no version information from manifest.yml is used (except for comment, we can't control comment as of now).
970
1739
  if version is not None:
@@ -972,7 +1741,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
972
1741
  "Ignoring version information from the application manifest since a version was explicitly specified with the command."
973
1742
  )
974
1743
  resolved_patch = patch
975
- resolved_label = label if label is not None else ""
1744
+ resolved_label = label
976
1745
  resolved_version = version
977
1746
 
978
1747
  # When version is not set by CLI, version name is read from manifest.yml. patch and label from CLI will be used, if provided.
@@ -1026,17 +1795,18 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1026
1795
  resolved_label = label if label is not None else label_manifest
1027
1796
 
1028
1797
  # Check if patch needs to throw a bad option error, either if application package does not exist or if version does not exist
1029
- if resolved_patch is not None:
1798
+ # If patch is 0 and version does not exist, it is a valid case, because patch 0 is the first patch in a version.
1799
+ if resolved_patch:
1030
1800
  try:
1031
- if not self.get_existing_version_info(resolved_version):
1801
+ if not self.get_existing_version_info(to_identifier(resolved_version)):
1032
1802
  raise BadOptionUsage(
1033
1803
  option_name="patch",
1034
- message=f"Cannot create patch {resolved_patch} when version {resolved_version} is not defined in the application package {self.name}. Try again without specifying a patch.",
1804
+ message=f"Version {resolved_version} is not defined in the application package {self.name}. Try again with a patch of 0 or without specifying any patch.",
1035
1805
  )
1036
1806
  except ApplicationPackageDoesNotExistError as app_err:
1037
1807
  raise BadOptionUsage(
1038
1808
  option_name="patch",
1039
- message=f"Cannot create patch {resolved_patch} when application package {self.name} does not exist. Try again without specifying a patch.",
1809
+ message=f"Application package {self.name} does not exist yet. Try again with a patch of 0 or without specifying any patch.",
1040
1810
  )
1041
1811
 
1042
1812
  return VersionInfo(