snowflake-cli 3.1.0__py3-none-any.whl → 3.2.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 (60) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
  3. snowflake/cli/_plugins/connection/commands.py +124 -109
  4. snowflake/cli/_plugins/connection/util.py +54 -9
  5. snowflake/cli/_plugins/cortex/manager.py +1 -1
  6. snowflake/cli/_plugins/git/manager.py +4 -4
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  9. snowflake/cli/_plugins/nativeapp/commands.py +10 -3
  10. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  11. snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
  13. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  14. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  15. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  16. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  17. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  19. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
  20. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
  21. snowflake/cli/_plugins/notebook/manager.py +2 -2
  22. snowflake/cli/_plugins/object/commands.py +10 -1
  23. snowflake/cli/_plugins/object/manager.py +13 -5
  24. snowflake/cli/_plugins/snowpark/common.py +3 -3
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
  26. snowflake/cli/_plugins/spcs/common.py +29 -0
  27. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  28. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  29. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  30. snowflake/cli/_plugins/spcs/services/commands.py +64 -13
  31. snowflake/cli/_plugins/spcs/services/manager.py +75 -15
  32. snowflake/cli/_plugins/sql/commands.py +9 -1
  33. snowflake/cli/_plugins/sql/manager.py +9 -4
  34. snowflake/cli/_plugins/stage/commands.py +20 -16
  35. snowflake/cli/_plugins/stage/diff.py +1 -1
  36. snowflake/cli/_plugins/stage/manager.py +140 -11
  37. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  38. snowflake/cli/_plugins/workspace/commands.py +6 -3
  39. snowflake/cli/api/cli_global_context.py +1 -0
  40. snowflake/cli/api/config.py +23 -5
  41. snowflake/cli/api/console/console.py +4 -19
  42. snowflake/cli/api/entities/utils.py +19 -32
  43. snowflake/cli/api/errno.py +2 -0
  44. snowflake/cli/api/exceptions.py +9 -0
  45. snowflake/cli/api/metrics.py +223 -7
  46. snowflake/cli/api/output/types.py +1 -1
  47. snowflake/cli/api/project/definition_conversion.py +179 -62
  48. snowflake/cli/api/rest_api.py +26 -4
  49. snowflake/cli/api/secure_utils.py +1 -1
  50. snowflake/cli/api/sql_execution.py +35 -22
  51. snowflake/cli/api/stage_path.py +5 -2
  52. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
  53. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
  54. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  55. snowflake/cli/_plugins/nativeapp/manager.py +0 -392
  56. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  57. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  58. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
  59. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  60. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from contextlib import contextmanager
4
+ import re
5
5
  from pathlib import Path
6
6
  from textwrap import dedent
7
7
  from typing import List, Literal, Optional, Union
@@ -11,6 +11,7 @@ from click import BadOptionUsage, ClickException
11
11
  from pydantic import Field, field_validator
12
12
  from snowflake.cli._plugins.nativeapp.artifacts import (
13
13
  BundleMap,
14
+ VersionInfo,
14
15
  build_bundle,
15
16
  find_version_info_in_manifest_file,
16
17
  )
@@ -40,12 +41,14 @@ from snowflake.cli._plugins.nativeapp.policy import (
40
41
  DenyAlwaysPolicy,
41
42
  PolicyBase,
42
43
  )
44
+ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
45
+ from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
46
+ InsufficientPrivilegesError,
47
+ )
43
48
  from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
44
49
  from snowflake.cli._plugins.stage.diff import DiffResult
45
50
  from snowflake.cli._plugins.stage.manager import StageManager
46
51
  from snowflake.cli._plugins.workspace.context import ActionContext
47
- from snowflake.cli.api.cli_global_context import get_cli_context
48
- from snowflake.cli.api.console.abc import AbstractConsole
49
52
  from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
50
53
  from snowflake.cli.api.entities.utils import (
51
54
  drop_generic_object,
@@ -56,7 +59,6 @@ from snowflake.cli.api.entities.utils import (
56
59
  )
57
60
  from snowflake.cli.api.errno import DOES_NOT_EXIST_OR_NOT_AUTHORIZED
58
61
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
59
- from snowflake.cli.api.metrics import CLICounterField
60
62
  from snowflake.cli.api.project.schemas.entities.common import (
61
63
  EntityModelBase,
62
64
  Identifier,
@@ -69,6 +71,7 @@ from snowflake.cli.api.project.schemas.updatable_model import (
69
71
  from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
70
72
  from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
71
73
  from snowflake.cli.api.project.util import (
74
+ SCHEMA_AND_NAME,
72
75
  append_test_resource_suffix,
73
76
  extract_schema,
74
77
  identifier_to_show_like_pattern,
@@ -109,8 +112,9 @@ class ApplicationPackageEntityModel(EntityModelBase):
109
112
  title="Distribution of the application package created by the Snowflake CLI",
110
113
  default="internal",
111
114
  )
112
- manifest: str = Field(
113
- title="Path to manifest.yml",
115
+ manifest: Optional[str] = Field(
116
+ title="Path to manifest.yml. Unused and deprecated starting with Snowflake CLI 3.2",
117
+ default="",
114
118
  )
115
119
 
116
120
  @field_validator("identifier")
@@ -143,25 +147,68 @@ class ApplicationPackageEntityModel(EntityModelBase):
143
147
 
144
148
  return transformed_artifacts
145
149
 
150
+ @field_validator("stage")
151
+ @classmethod
152
+ def validate_source_stage(cls, input_value: str):
153
+ if not re.match(SCHEMA_AND_NAME, input_value):
154
+ raise ValueError(
155
+ "Incorrect value for stage of native_app. Expected format for this field is {schema_name}.{stage_name} "
156
+ )
157
+ return input_value
158
+
146
159
 
147
160
  class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
148
161
  """
149
162
  A Native App application package.
150
163
  """
151
164
 
152
- def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
165
+ @property
166
+ def project_root(self) -> Path:
167
+ return self._workspace_ctx.project_root
168
+
169
+ @property
170
+ def deploy_root(self) -> Path:
171
+ return self.project_root / self._entity_model.deploy_root
172
+
173
+ @property
174
+ def bundle_root(self) -> Path:
175
+ return self.project_root / self._entity_model.bundle_root
176
+
177
+ @property
178
+ def generated_root(self) -> Path:
179
+ return self.deploy_root / self._entity_model.generated_root
180
+
181
+ @property
182
+ def name(self) -> str:
183
+ return self._entity_model.fqn.name
184
+
185
+ @property
186
+ def role(self) -> str:
153
187
  model = self._entity_model
154
- workspace_ctx = self._workspace_ctx
155
- return self.bundle(
156
- project_root=workspace_ctx.project_root,
157
- deploy_root=workspace_ctx.project_root / model.deploy_root,
158
- bundle_root=workspace_ctx.project_root / model.bundle_root,
159
- generated_root=(
160
- workspace_ctx.project_root / model.deploy_root / model.generated_root
161
- ),
162
- package_name=model.identifier,
163
- artifacts=model.artifacts,
164
- )
188
+ return (model.meta and model.meta.role) or self._workspace_ctx.default_role
189
+
190
+ @property
191
+ def warehouse(self) -> str:
192
+ model = self._entity_model
193
+ return (
194
+ model.meta and model.meta.warehouse and to_identifier(model.meta.warehouse)
195
+ ) or to_identifier(self._workspace_ctx.default_warehouse)
196
+
197
+ @property
198
+ def stage_fqn(self) -> str:
199
+ return f"{self.name}.{self._entity_model.stage}"
200
+
201
+ @property
202
+ def scratch_stage_fqn(self) -> str:
203
+ return f"{self.name}.{self._entity_model.scratch_stage}"
204
+
205
+ @property
206
+ def post_deploy_hooks(self) -> list[PostDeployHook] | None:
207
+ model = self._entity_model
208
+ return model.meta and model.meta.post_deploy
209
+
210
+ def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
211
+ return self._bundle()
165
212
 
166
213
  def action_deploy(
167
214
  self,
@@ -176,57 +223,94 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
176
223
  *args,
177
224
  **kwargs,
178
225
  ):
179
- model = self._entity_model
180
- workspace_ctx = self._workspace_ctx
181
- package_name = model.fqn.identifier
182
-
183
- if force:
184
- policy = AllowAlwaysPolicy()
185
- elif interactive:
186
- policy = AskAlwaysPolicy()
187
- else:
188
- policy = DenyAlwaysPolicy()
189
-
190
- return self.deploy(
191
- console=workspace_ctx.console,
192
- project_root=workspace_ctx.project_root,
193
- deploy_root=workspace_ctx.project_root / model.deploy_root,
194
- bundle_root=workspace_ctx.project_root / model.bundle_root,
195
- generated_root=(
196
- workspace_ctx.project_root / model.deploy_root / model.generated_root
197
- ),
198
- artifacts=model.artifacts,
226
+ return self._deploy(
199
227
  bundle_map=None,
200
- package_name=package_name,
201
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
202
- package_distribution=model.distribution,
203
228
  prune=prune,
204
229
  recursive=recursive,
205
230
  paths=paths,
206
231
  print_diff=True,
207
232
  validate=validate,
208
- stage_fqn=stage_fqn or f"{package_name}.{model.stage}",
209
- package_warehouse=(
210
- (model.meta and model.meta.warehouse) or workspace_ctx.default_warehouse
211
- ),
212
- post_deploy_hooks=model.meta and model.meta.post_deploy,
213
- policy=policy,
233
+ stage_fqn=stage_fqn or self.stage_fqn,
234
+ interactive=interactive,
235
+ force=force,
214
236
  )
215
237
 
216
238
  def action_drop(self, action_ctx: ActionContext, force_drop: bool, *args, **kwargs):
217
- model = self._entity_model
218
- workspace_ctx = self._workspace_ctx
219
- package_name = model.fqn.identifier
220
- if model.meta and model.meta.role:
221
- package_role = model.meta.role
239
+ console = self._workspace_ctx.console
240
+ sql_executor = get_sql_executor()
241
+ needs_confirm = True
242
+
243
+ # 1. If existing application package is not found, exit gracefully
244
+ show_obj_row = self.get_existing_app_pkg_info()
245
+ if show_obj_row is None:
246
+ console.warning(
247
+ f"Role {self.role} does not own any application package with the name {self.name}, or the application package does not exist."
248
+ )
249
+ return
250
+
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
+ )
266
+
267
+ # 3. Check distribution of the existing application package
268
+ actual_distribution = self.get_app_pkg_distribution_in_snowflake()
269
+ if not self.verify_project_distribution():
270
+ console.warning(
271
+ f"Dropping application package {self.name} with distribution '{actual_distribution}'."
272
+ )
273
+
274
+ # 4. If distribution is internal, check if created by the Snowflake CLI
275
+ row_comment = show_obj_row[COMMENT_COL]
276
+ if actual_distribution == INTERNAL_DISTRIBUTION:
277
+ if row_comment in ALLOWED_SPECIAL_COMMENTS:
278
+ needs_confirm = False
279
+ else:
280
+ if needs_confirmation(needs_confirm, force_drop):
281
+ console.warning(
282
+ f"Application package {self.name} was not created by Snowflake CLI."
283
+ )
222
284
  else:
223
- package_role = workspace_ctx.default_role
285
+ if needs_confirmation(needs_confirm, force_drop):
286
+ console.warning(
287
+ f"Application package {self.name} in your Snowflake account has distribution property '{EXTERNAL_DISTRIBUTION}' and could be associated with one or more of your listings on Snowflake Marketplace."
288
+ )
224
289
 
225
- self.drop(
226
- console=workspace_ctx.console,
227
- package_name=package_name,
228
- package_role=package_role,
229
- force_drop=force_drop,
290
+ if needs_confirmation(needs_confirm, force_drop):
291
+ should_drop_object = typer.confirm(
292
+ dedent(
293
+ f"""\
294
+ Application package details:
295
+ Name: {self.name}
296
+ Created on: {show_obj_row["created_on"]}
297
+ Distribution: {actual_distribution}
298
+ Owner: {show_obj_row[OWNER_COL]}
299
+ Comment: {show_obj_row[COMMENT_COL]}
300
+ Are you sure you want to drop it?
301
+ """
302
+ )
303
+ )
304
+ if not should_drop_object:
305
+ console.message(f"Did not drop application package {self.name}.")
306
+ return # The user desires to keep the application package, therefore exit gracefully
307
+
308
+ # All validations have passed, drop object
309
+ drop_generic_object(
310
+ console=console,
311
+ object_type="application package",
312
+ object_name=(self.name),
313
+ role=(self.role),
230
314
  )
231
315
 
232
316
  def action_validate(
@@ -238,92 +322,129 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
238
322
  *args,
239
323
  **kwargs,
240
324
  ):
241
- model = self._entity_model
242
- workspace_ctx = self._workspace_ctx
243
- package_name = model.fqn.identifier
244
- if force:
245
- policy = AllowAlwaysPolicy()
246
- elif interactive:
247
- policy = AskAlwaysPolicy()
248
- else:
249
- policy = DenyAlwaysPolicy()
250
-
251
325
  self.validate_setup_script(
252
- console=workspace_ctx.console,
253
- project_root=workspace_ctx.project_root,
254
- deploy_root=workspace_ctx.project_root / model.deploy_root,
255
- bundle_root=workspace_ctx.project_root / model.bundle_root,
256
- generated_root=(
257
- workspace_ctx.project_root / model.deploy_root / model.generated_root
258
- ),
259
- artifacts=model.artifacts,
260
- package_name=package_name,
261
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
262
- package_distribution=model.distribution,
263
- prune=True,
264
- recursive=True,
265
- paths=[],
266
- stage_fqn=f"{package_name}.{model.stage}",
267
- package_warehouse=(
268
- (model.meta and model.meta.warehouse) or workspace_ctx.default_warehouse
269
- ),
270
- policy=policy,
271
326
  use_scratch_stage=use_scratch_stage,
272
- scratch_stage_fqn=f"{package_name}.{model.scratch_stage}",
327
+ interactive=interactive,
328
+ force=force,
273
329
  )
274
- workspace_ctx.console.message("Setup script is valid")
330
+ self._workspace_ctx.console.message("Setup script is valid")
275
331
 
276
332
  def action_version_list(
277
333
  self, action_ctx: ActionContext, *args, **kwargs
278
334
  ) -> SnowflakeCursor:
279
- model = self._entity_model
280
- workspace_ctx = self._workspace_ctx
281
- return self.version_list(
282
- package_name=model.fqn.identifier,
283
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
284
- )
335
+ """
336
+ Get all existing versions, if defined, for an application package.
337
+ It executes a 'show versions in application package' query and returns all the results.
338
+ """
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
285
348
 
286
349
  def action_version_create(
287
350
  self,
288
351
  action_ctx: ActionContext,
289
352
  version: Optional[str],
290
353
  patch: Optional[int],
354
+ label: Optional[str],
291
355
  skip_git_check: bool,
292
356
  interactive: bool,
293
357
  force: bool,
294
358
  *args,
295
359
  **kwargs,
296
360
  ):
297
- model = self._entity_model
298
- workspace_ctx = self._workspace_ctx
299
- package_name = model.fqn.identifier
300
- return self.version_create(
301
- console=workspace_ctx.console,
302
- project_root=workspace_ctx.project_root,
303
- deploy_root=workspace_ctx.project_root / model.deploy_root,
304
- bundle_root=workspace_ctx.project_root / model.bundle_root,
305
- generated_root=(
306
- workspace_ctx.project_root / model.deploy_root / model.generated_root
307
- ),
308
- artifacts=model.artifacts,
309
- package_name=package_name,
310
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
311
- package_distribution=model.distribution,
361
+ """
362
+ Create a version and/or patch for a new or existing application package.
363
+ Always performs a deploy action before creating version or patch.
364
+ If version is not provided in CLI, bundle is performed to read version from manifest.yml. Raises a ClickException if version is not found.
365
+ """
366
+ console = self._workspace_ctx.console
367
+
368
+ if force:
369
+ policy = AllowAlwaysPolicy()
370
+ elif interactive:
371
+ policy = AskAlwaysPolicy()
372
+ else:
373
+ policy = DenyAlwaysPolicy()
374
+
375
+ if skip_git_check:
376
+ git_policy = DenyAlwaysPolicy()
377
+ else:
378
+ git_policy = AllowAlwaysPolicy()
379
+
380
+ bundle_map = self._bundle()
381
+ resolved_version, resolved_patch, resolved_label = self.resolve_version_info(
382
+ version=version,
383
+ patch=patch,
384
+ label=label,
385
+ bundle_map=bundle_map,
386
+ policy=policy,
387
+ interactive=interactive,
388
+ )
389
+
390
+ if git_policy.should_proceed():
391
+ self.check_index_changes_in_git_repo(policy=policy, interactive=interactive)
392
+
393
+ self._deploy(
394
+ bundle_map=bundle_map,
312
395
  prune=True,
313
396
  recursive=True,
314
- paths=None,
397
+ paths=[],
315
398
  print_diff=True,
316
399
  validate=True,
317
- stage_fqn=f"{package_name}.{model.stage}",
318
- package_warehouse=(
319
- (model.meta and model.meta.warehouse) or workspace_ctx.default_warehouse
320
- ),
321
- post_deploy_hooks=model.meta and model.meta.post_deploy,
322
- version=version,
323
- patch=patch,
324
- skip_git_check=skip_git_check,
325
- force=force,
400
+ stage_fqn=self.stage_fqn,
326
401
  interactive=interactive,
402
+ force=force,
403
+ )
404
+
405
+ # Warn if the version exists in a release directive(s)
406
+ try:
407
+ existing_release_directives = (
408
+ self.get_existing_release_directive_info_for_version(resolved_version)
409
+ )
410
+ except InsufficientPrivilegesError:
411
+ warning = (
412
+ "Could not check for existing release directives due to insufficient privileges. "
413
+ "The MANAGE RELEASES privilege is required to check for existing release directives."
414
+ )
415
+ else:
416
+ if existing_release_directives:
417
+ release_directive_names = ", ".join(
418
+ row["name"] for row in existing_release_directives
419
+ )
420
+ warning = f"Version {resolved_version} already defined in application package {self.name} and in release directive(s): {release_directive_names}."
421
+ else:
422
+ warning = ""
423
+
424
+ if warning:
425
+ console.warning(warning)
426
+ user_prompt = (
427
+ f"Are you sure you want to create a new patch for version {resolved_version} in application "
428
+ f"package {self.name}? Once added, this operation cannot be undone."
429
+ )
430
+ if not policy.should_proceed(user_prompt):
431
+ if interactive:
432
+ console.message("Not creating a new patch.")
433
+ raise typer.Exit(0)
434
+ else:
435
+ console.message(
436
+ "Cannot create a new patch non-interactively without --force."
437
+ )
438
+ raise typer.Exit(1)
439
+
440
+ # Define a new version in the application package
441
+ if not self.get_existing_version_info(resolved_version):
442
+ 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.
444
+
445
+ # Add a new patch to an existing (old) version
446
+ self.add_new_patch_to_version(
447
+ version=resolved_version, patch=resolved_patch, label=resolved_label
327
448
  )
328
449
 
329
450
  def action_version_drop(
@@ -335,104 +456,148 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
335
456
  *args,
336
457
  **kwargs,
337
458
  ):
338
- model = self._entity_model
339
- workspace_ctx = self._workspace_ctx
340
- package_name = model.fqn.identifier
341
- return self.version_drop(
342
- console=workspace_ctx.console,
343
- project_root=workspace_ctx.project_root,
344
- deploy_root=workspace_ctx.project_root / model.deploy_root,
345
- bundle_root=workspace_ctx.project_root / model.bundle_root,
346
- generated_root=(
347
- workspace_ctx.project_root / model.deploy_root / model.generated_root
348
- ),
349
- artifacts=model.artifacts,
350
- package_name=package_name,
351
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
352
- package_distribution=model.distribution,
353
- version=version,
354
- force=force,
355
- interactive=interactive,
459
+ """
460
+ Drops a version defined in an application package. If --force is provided, then no user prompts will be executed.
461
+ """
462
+ console = self._workspace_ctx.console
463
+
464
+ if force:
465
+ interactive = False
466
+ policy = AllowAlwaysPolicy()
467
+ else:
468
+ policy = AskAlwaysPolicy() if interactive else DenyAlwaysPolicy()
469
+
470
+ # 1. Check for existing an existing application package
471
+ show_obj_row = self.get_existing_app_pkg_info()
472
+ if not show_obj_row:
473
+ raise ApplicationPackageDoesNotExistError(self.name)
474
+
475
+ # 2. Check distribution of the existing application package
476
+ actual_distribution = self.get_app_pkg_distribution_in_snowflake()
477
+ if not self.verify_project_distribution(
478
+ expected_distribution=actual_distribution
479
+ ):
480
+ console.warning(
481
+ f"Continuing to execute version drop on application package "
482
+ f"{self.name} with distribution '{actual_distribution}'."
483
+ )
484
+
485
+ # 3. If the user did not pass in a version string, determine from manifest.yml
486
+ if not version:
487
+ console.message(
488
+ dedent(
489
+ f"""\
490
+ Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead.
491
+ This step will bundle your app artifacts to determine the location of the manifest.yml file.
492
+ """
493
+ )
494
+ )
495
+ self._bundle()
496
+ version_info = find_version_info_in_manifest_file(self.deploy_root)
497
+ version = version_info.version_name
498
+ if not version:
499
+ raise ClickException(
500
+ "Manifest.yml file does not contain a value for the version field."
501
+ )
502
+
503
+ # Make the version a valid identifier, adding quotes if necessary
504
+ version = to_identifier(version)
505
+
506
+ console.step(
507
+ f"About to drop version {version} in application package {self.name}."
356
508
  )
357
509
 
358
- @staticmethod
359
- def bundle(
360
- project_root: Path,
361
- deploy_root: Path,
362
- bundle_root: Path,
363
- generated_root: Path,
364
- artifacts: list[PathMapping],
365
- package_name: str,
366
- ):
367
- bundle_map = build_bundle(project_root, deploy_root, artifacts)
510
+ # If user did not provide --force, ask for confirmation
511
+ user_prompt = (
512
+ f"Are you sure you want to drop version {version} "
513
+ f"in application package {self.name}? "
514
+ f"Once dropped, this operation cannot be undone."
515
+ )
516
+ if not policy.should_proceed(user_prompt):
517
+ if interactive:
518
+ console.message("Not dropping version.")
519
+ raise typer.Exit(0)
520
+ else:
521
+ console.message(
522
+ "Cannot drop version non-interactively without --force."
523
+ )
524
+ raise typer.Exit(1)
525
+
526
+ # 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)
535
+
536
+ console.message(
537
+ f"Version {version} in application package {self.name} dropped successfully."
538
+ )
539
+
540
+ def _bundle(self):
541
+ model = self._entity_model
542
+ bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts)
368
543
  bundle_context = BundleContext(
369
- package_name=package_name,
370
- artifacts=artifacts,
371
- project_root=project_root,
372
- bundle_root=bundle_root,
373
- deploy_root=deploy_root,
374
- generated_root=generated_root,
544
+ package_name=self.name,
545
+ artifacts=model.artifacts,
546
+ project_root=self.project_root,
547
+ bundle_root=self.bundle_root,
548
+ deploy_root=self.deploy_root,
549
+ generated_root=self.generated_root,
375
550
  )
376
551
  compiler = NativeAppCompiler(bundle_context)
377
552
  compiler.compile_artifacts()
378
553
  return bundle_map
379
554
 
380
- @classmethod
381
- def deploy(
382
- cls,
383
- console: AbstractConsole,
384
- project_root: Path,
385
- deploy_root: Path,
386
- bundle_root: Path,
387
- generated_root: Path,
388
- artifacts: list[PathMapping],
555
+ def _deploy(
556
+ self,
389
557
  bundle_map: BundleMap | None,
390
- package_name: str,
391
- package_role: str,
392
- package_distribution: str,
393
- package_warehouse: str | None,
394
558
  prune: bool,
395
559
  recursive: bool,
396
- paths: List[Path] | None,
560
+ paths: list[Path],
397
561
  print_diff: bool,
398
562
  validate: bool,
399
563
  stage_fqn: str,
400
- post_deploy_hooks: list[PostDeployHook] | None,
401
- policy: PolicyBase,
564
+ interactive: bool,
565
+ force: bool,
566
+ run_post_deploy_hooks: bool = True,
402
567
  ) -> DiffResult:
568
+ model = self._entity_model
569
+ workspace_ctx = self._workspace_ctx
570
+ if force:
571
+ policy = AllowAlwaysPolicy()
572
+ elif interactive:
573
+ policy = AskAlwaysPolicy()
574
+ else:
575
+ policy = DenyAlwaysPolicy()
576
+
577
+ console = workspace_ctx.console
578
+ stage_fqn = stage_fqn or self.stage_fqn
579
+
403
580
  # 1. Create a bundle if one wasn't passed in
404
- bundle_map = bundle_map or cls.bundle(
405
- project_root=project_root,
406
- deploy_root=deploy_root,
407
- bundle_root=bundle_root,
408
- generated_root=generated_root,
409
- artifacts=artifacts,
410
- package_name=package_name,
411
- )
581
+ bundle_map = bundle_map or self._bundle()
412
582
 
413
583
  # 2. Create an empty application package, if none exists
414
584
  try:
415
- cls.create_app_package(
416
- console=console,
417
- package_name=package_name,
418
- package_role=package_role,
419
- package_distribution=package_distribution,
420
- )
585
+ self.create_app_package()
421
586
  except ApplicationPackageAlreadyExistsError as e:
422
587
  console.warning(e.message)
423
588
  if not policy.should_proceed("Proceed with using this package?"):
424
589
  raise typer.Abort() from e
425
590
 
426
- with get_sql_executor().use_role(package_role):
591
+ with get_sql_executor().use_role(self.role):
427
592
  # 3. Upload files from deploy root local folder to the above stage
428
593
  stage_schema = extract_schema(stage_fqn)
429
594
  diff = sync_deploy_root_with_stage(
430
595
  console=console,
431
- deploy_root=deploy_root,
432
- package_name=package_name,
596
+ deploy_root=self.deploy_root,
597
+ package_name=self.name,
433
598
  stage_schema=stage_schema,
434
599
  bundle_map=bundle_map,
435
- role=package_role,
600
+ role=self.role,
436
601
  prune=prune,
437
602
  recursive=recursive,
438
603
  stage_fqn=stage_fqn,
@@ -440,228 +605,19 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
440
605
  print_diff=print_diff,
441
606
  )
442
607
 
443
- cls.execute_post_deploy_hooks(
444
- console=console,
445
- project_root=project_root,
446
- post_deploy_hooks=post_deploy_hooks,
447
- package_name=package_name,
448
- package_warehouse=package_warehouse,
449
- )
608
+ if run_post_deploy_hooks:
609
+ self.execute_post_deploy_hooks()
450
610
 
451
611
  if validate:
452
- cls.validate_setup_script(
453
- console=console,
454
- project_root=project_root,
455
- deploy_root=deploy_root,
456
- bundle_root=bundle_root,
457
- generated_root=generated_root,
458
- artifacts=artifacts,
459
- package_name=package_name,
460
- package_role=package_role,
461
- package_distribution=package_distribution,
462
- prune=prune,
463
- recursive=recursive,
464
- paths=paths,
465
- stage_fqn=stage_fqn,
466
- package_warehouse=package_warehouse,
467
- policy=policy,
612
+ self.validate_setup_script(
468
613
  use_scratch_stage=False,
469
- scratch_stage_fqn="",
614
+ interactive=interactive,
615
+ force=force,
470
616
  )
471
617
 
472
618
  return diff
473
619
 
474
- @staticmethod
475
- def version_list(package_name: str, package_role: str) -> SnowflakeCursor:
476
- """
477
- Get all existing versions, if defined, for an application package.
478
- It executes a 'show versions in application package' query and returns all the results.
479
- """
480
- sql_executor = get_sql_executor()
481
- with sql_executor.use_role(package_role):
482
- show_obj_query = f"show versions in application package {package_name}"
483
- show_obj_cursor = sql_executor.execute_query(show_obj_query)
484
-
485
- if show_obj_cursor.rowcount is None:
486
- raise SnowflakeSQLExecutionError(show_obj_query)
487
-
488
- return show_obj_cursor
489
-
490
- @classmethod
491
- def version_create(
492
- cls,
493
- console: AbstractConsole,
494
- project_root: Path,
495
- deploy_root: Path,
496
- bundle_root: Path,
497
- generated_root: Path,
498
- artifacts: list[PathMapping],
499
- package_name: str,
500
- package_role: str,
501
- package_distribution: str,
502
- package_warehouse: str | None,
503
- prune: bool,
504
- recursive: bool,
505
- paths: List[Path] | None,
506
- print_diff: bool,
507
- validate: bool,
508
- stage_fqn: str,
509
- post_deploy_hooks: list[PostDeployHook] | None,
510
- version: Optional[str],
511
- patch: Optional[int],
512
- force: bool,
513
- interactive: bool,
514
- skip_git_check: bool,
515
- ):
516
- """
517
- Perform bundle, application package creation, stage upload, version and/or patch to an application package.
518
- """
519
- is_interactive = False
520
- if force:
521
- policy = AllowAlwaysPolicy()
522
- elif interactive:
523
- is_interactive = True
524
- policy = AskAlwaysPolicy()
525
- else:
526
- policy = DenyAlwaysPolicy()
527
-
528
- if skip_git_check:
529
- git_policy = DenyAlwaysPolicy()
530
- else:
531
- git_policy = AllowAlwaysPolicy()
532
-
533
- # Make sure version is not None before proceeding any further.
534
- # This will raise an exception if version information is not found. Patch can be None.
535
- bundle_map = None
536
- if not version:
537
- console.message(
538
- dedent(
539
- f"""\
540
- Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead.
541
- This step will bundle your app artifacts to determine the location of the manifest.yml file.
542
- """
543
- )
544
- )
545
- bundle_map = cls.bundle(
546
- project_root=project_root,
547
- deploy_root=deploy_root,
548
- bundle_root=bundle_root,
549
- generated_root=generated_root,
550
- artifacts=artifacts,
551
- package_name=package_name,
552
- )
553
- version, patch = find_version_info_in_manifest_file(deploy_root)
554
- if not version:
555
- raise ClickException(
556
- "Manifest.yml file does not contain a value for the version field."
557
- )
558
-
559
- # Check if --patch needs to throw a bad option error, either if application package does not exist or if version does not exist
560
- if patch is not None:
561
- try:
562
- if not cls.get_existing_version_info(
563
- version, package_name, package_role
564
- ):
565
- raise BadOptionUsage(
566
- option_name="patch",
567
- message=f"Cannot create a custom patch when version {version} is not defined in the application package {package_name}. Try again without using --patch.",
568
- )
569
- except ApplicationPackageDoesNotExistError as app_err:
570
- raise BadOptionUsage(
571
- option_name="patch",
572
- message=f"Cannot create a custom patch when application package {package_name} does not exist. Try again without using --patch.",
573
- )
574
-
575
- if git_policy.should_proceed():
576
- cls.check_index_changes_in_git_repo(
577
- console=console,
578
- project_root=project_root,
579
- policy=policy,
580
- is_interactive=is_interactive,
581
- )
582
-
583
- cls.deploy(
584
- console=console,
585
- project_root=project_root,
586
- deploy_root=deploy_root,
587
- bundle_root=bundle_root,
588
- generated_root=generated_root,
589
- artifacts=artifacts,
590
- bundle_map=bundle_map,
591
- package_name=package_name,
592
- package_role=package_role,
593
- package_distribution=package_distribution,
594
- prune=prune,
595
- recursive=recursive,
596
- paths=paths,
597
- print_diff=print_diff,
598
- validate=validate,
599
- stage_fqn=stage_fqn,
600
- package_warehouse=package_warehouse,
601
- post_deploy_hooks=post_deploy_hooks,
602
- policy=policy,
603
- )
604
-
605
- # Warn if the version exists in a release directive(s)
606
- existing_release_directives = (
607
- cls.get_existing_release_directive_info_for_version(
608
- package_name, package_role, version
609
- )
610
- )
611
-
612
- if existing_release_directives:
613
- release_directive_names = ", ".join(
614
- row["name"] for row in existing_release_directives
615
- )
616
- console.warning(
617
- dedent(
618
- f"""\
619
- Version {version} already defined in application package {package_name} and in release directive(s): {release_directive_names}.
620
- """
621
- )
622
- )
623
-
624
- user_prompt = (
625
- f"Are you sure you want to create a new patch for version {version} in application "
626
- f"package {package_name}? Once added, this operation cannot be undone."
627
- )
628
- if not policy.should_proceed(user_prompt):
629
- if is_interactive:
630
- console.message("Not creating a new patch.")
631
- raise typer.Exit(0)
632
- else:
633
- console.message(
634
- "Cannot create a new patch non-interactively without --force."
635
- )
636
- raise typer.Exit(1)
637
-
638
- # Define a new version in the application package
639
- if not cls.get_existing_version_info(version, package_name, package_role):
640
- cls.add_new_version(
641
- console=console,
642
- package_name=package_name,
643
- package_role=package_role,
644
- stage_fqn=stage_fqn,
645
- version=version,
646
- )
647
- return # A new version created automatically has patch 0, we do not need to further increment the patch.
648
-
649
- # Add a new patch to an existing (old) version
650
- cls.add_new_patch_to_version(
651
- console=console,
652
- package_name=package_name,
653
- package_role=package_role,
654
- stage_fqn=stage_fqn,
655
- version=version,
656
- patch=patch,
657
- )
658
-
659
- @staticmethod
660
- def get_existing_version_info(
661
- version: str,
662
- package_name: str,
663
- package_role: str,
664
- ) -> Optional[dict]:
620
+ def get_existing_version_info(self, version: str) -> Optional[dict]:
665
621
  """
666
622
  Get the latest patch on an existing version by name in the application package.
667
623
  Executes 'show versions like ... in application package' query and returns
@@ -669,9 +625,9 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
669
625
  returns None.
670
626
  """
671
627
  sql_executor = get_sql_executor()
672
- with sql_executor.use_role(package_role):
628
+ with sql_executor.use_role(self.role):
673
629
  try:
674
- query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {package_name}"
630
+ query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {self.name}"
675
631
  cursor = sql_executor.execute_query(query, cursor_class=DictCursor)
676
632
 
677
633
  if cursor.rowcount is None:
@@ -688,116 +644,75 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
688
644
 
689
645
  except ProgrammingError as err:
690
646
  if err.msg.__contains__("does not exist or not authorized"):
691
- raise ApplicationPackageDoesNotExistError(package_name)
647
+ raise ApplicationPackageDoesNotExistError(self.name)
692
648
  else:
693
649
  generic_sql_error_handler(err=err)
694
650
  return None
695
651
 
696
- @classmethod
697
652
  def get_existing_release_directive_info_for_version(
698
- cls,
699
- package_name: str,
700
- package_role: str,
701
- version: str,
653
+ self, version: str
702
654
  ) -> List[dict]:
703
655
  """
704
656
  Get all existing release directives, if present, set on the version defined in an application package.
705
657
  It executes a 'show release directives in application package' query and returns the filtered results, if they exist.
706
658
  """
707
- sql_executor = get_sql_executor()
708
- with sql_executor.use_role(package_role):
709
- show_obj_query = (
710
- f"show release directives in application package {package_name}"
711
- )
712
- show_obj_cursor = sql_executor.execute_query(
713
- show_obj_query, cursor_class=DictCursor
714
- )
715
-
716
- if show_obj_cursor.rowcount is None:
717
- raise SnowflakeSQLExecutionError(show_obj_query)
718
-
719
- show_obj_rows = find_all_rows(
720
- show_obj_cursor,
721
- lambda row: row[VERSION_COL] == unquote_identifier(version),
722
- )
723
-
724
- return show_obj_rows
659
+ release_directives = get_snowflake_facade().show_release_directives(
660
+ self.name, self.role
661
+ )
662
+ return [
663
+ directive
664
+ for directive in release_directives
665
+ if directive[VERSION_COL] == unquote_identifier(version)
666
+ ]
725
667
 
726
- @classmethod
727
- def add_new_version(
728
- cls,
729
- console: AbstractConsole,
730
- package_name: str,
731
- package_role: str,
732
- stage_fqn: str,
733
- version: str,
734
- ) -> None:
668
+ def add_new_version(self, version: str, label: str | None = None) -> None:
735
669
  """
736
- Defines a new version in an existing application package.
670
+ Add a new version with an optional label in application package.
737
671
  """
738
- # Make the version a valid identifier, adding quotes if necessary
739
- version = to_identifier(version)
740
- sql_executor = get_sql_executor()
741
- with sql_executor.use_role(package_role):
742
- console.step(
743
- f"Defining a new version {version} in application package {package_name}"
744
- )
745
- add_version_query = dedent(
746
- f"""\
747
- alter application package {package_name}
748
- add version {version}
749
- using @{stage_fqn}
750
- """
751
- )
752
- sql_executor.execute_query(add_version_query, cursor_class=DictCursor)
753
- console.message(
754
- f"Version {version} created for application package {package_name}."
755
- )
672
+ console = self._workspace_ctx.console
673
+ with_label_prompt = f" labeled {label}" if label else ""
674
+
675
+ console.step(
676
+ f"Defining a new version {version}{with_label_prompt} in application package {self.name}"
677
+ )
678
+ get_snowflake_facade().create_version_in_package(
679
+ role=self.role,
680
+ package_name=self.name,
681
+ stage_fqn=self.stage_fqn,
682
+ version=version,
683
+ label=label,
684
+ )
685
+ console.message(
686
+ f"Version {version}{with_label_prompt} created for application package {self.name}."
687
+ )
756
688
 
757
- @classmethod
758
689
  def add_new_patch_to_version(
759
- cls,
760
- console: AbstractConsole,
761
- package_name: str,
762
- package_role: str,
763
- stage_fqn: str,
764
- version: str,
765
- patch: Optional[int] = None,
690
+ self, version: str, patch: int | None = None, label: str | None = None
766
691
  ):
767
692
  """
768
693
  Add a new patch, optionally a custom one, to an existing version in an application package.
769
694
  """
770
- # Make the version a valid identifier, adding quotes if necessary
771
- version = to_identifier(version)
772
- sql_executor = get_sql_executor()
773
- with sql_executor.use_role(package_role):
774
- console.step(
775
- f"Adding new patch to version {version} defined in application package {package_name}"
776
- )
777
- add_version_query = dedent(
778
- f"""\
779
- alter application package {package_name}
780
- add patch {patch if patch else ""} for version {version}
781
- using @{stage_fqn}
782
- """
783
- )
784
- result_cursor = sql_executor.execute_query(
785
- add_version_query, cursor_class=DictCursor
786
- )
695
+ console = self._workspace_ctx.console
787
696
 
788
- show_row = result_cursor.fetchall()[0]
789
- new_patch = show_row["patch"]
790
- console.message(
791
- f"Patch {new_patch} created for version {version} defined in application package {package_name}."
792
- )
697
+ with_label_prompt = f" labeled {label}" if label else ""
698
+
699
+ console.step(
700
+ f"Adding new patch to version {version}{with_label_prompt} defined in application package {self.name}"
701
+ )
702
+ new_patch = get_snowflake_facade().add_patch_to_package_version(
703
+ role=self.role,
704
+ package_name=self.name,
705
+ stage_fqn=self.stage_fqn,
706
+ version=version,
707
+ patch=patch,
708
+ label=label,
709
+ )
710
+ console.message(
711
+ f"Patch {new_patch}{with_label_prompt} created for version {version} defined in application package {self.name}."
712
+ )
793
713
 
794
- @classmethod
795
714
  def check_index_changes_in_git_repo(
796
- cls,
797
- console: AbstractConsole,
798
- project_root: Path,
799
- policy: PolicyBase,
800
- is_interactive: bool,
715
+ self, policy: PolicyBase, interactive: bool
801
716
  ) -> None:
802
717
  """
803
718
  Checks if the project root, i.e. the native apps project is a git repository. If it is a git repository,
@@ -807,8 +722,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
807
722
  from git import Repo
808
723
  from git.exc import InvalidGitRepositoryError
809
724
 
725
+ console = self._workspace_ctx.console
726
+
810
727
  try:
811
- repo = Repo(project_root, search_parent_directories=True)
728
+ repo = Repo(self.project_root, search_parent_directories=True)
812
729
  assert repo.git_dir is not None
813
730
 
814
731
  # Check if the repo has any changes, including untracked files
@@ -824,7 +741,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
824
741
  "Do you still want to continue?"
825
742
  )
826
743
  if not policy.should_proceed(user_prompt):
827
- if is_interactive:
744
+ if interactive:
828
745
  console.message("Not creating a new version.")
829
746
  raise typer.Exit(0)
830
747
  else:
@@ -836,141 +753,26 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
836
753
  except InvalidGitRepositoryError:
837
754
  pass # not a git repository, which is acceptable
838
755
 
839
- @classmethod
840
- def version_drop(
841
- cls,
842
- console: AbstractConsole,
843
- project_root: Path,
844
- deploy_root: Path,
845
- bundle_root: Path,
846
- generated_root: Path,
847
- artifacts: list[PathMapping],
848
- package_name: str,
849
- package_role: str,
850
- package_distribution: str,
851
- version: Optional[str],
852
- force: bool,
853
- interactive: bool,
854
- ):
855
- """
856
- Drops a version defined in an application package. If --force is provided, then no user prompts will be executed.
857
- """
858
- if force:
859
- interactive = False
860
- policy = AllowAlwaysPolicy()
861
- else:
862
- policy = AskAlwaysPolicy() if interactive else DenyAlwaysPolicy()
863
-
864
- # 1. Check for existing an existing application package
865
- show_obj_row = cls.get_existing_app_pkg_info(package_name, package_role)
866
- if not show_obj_row:
867
- raise ApplicationPackageDoesNotExistError(package_name)
868
-
869
- # 2. Check distribution of the existing application package
870
- actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
871
- package_name, package_role
872
- )
873
- if not cls.verify_project_distribution(
874
- console=console,
875
- package_name=package_name,
876
- package_role=package_role,
877
- package_distribution=package_distribution,
878
- expected_distribution=actual_distribution,
879
- ):
880
- console.warning(
881
- f"Continuing to execute version drop on application package "
882
- f"{package_name} with distribution '{actual_distribution}'."
883
- )
884
-
885
- # 3. If the user did not pass in a version string, determine from manifest.yml
886
- if not version:
887
- console.message(
888
- dedent(
889
- f"""\
890
- Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead.
891
- This step will bundle your app artifacts to determine the location of the manifest.yml file.
892
- """
893
- )
894
- )
895
- cls.bundle(
896
- project_root=project_root,
897
- deploy_root=deploy_root,
898
- bundle_root=bundle_root,
899
- generated_root=generated_root,
900
- artifacts=artifacts,
901
- package_name=package_name,
902
- )
903
- version, _ = find_version_info_in_manifest_file(deploy_root)
904
- if not version:
905
- raise ClickException(
906
- "Manifest.yml file does not contain a value for the version field."
907
- )
908
-
909
- # Make the version a valid identifier, adding quotes if necessary
910
- version = to_identifier(version)
911
-
912
- console.step(
913
- f"About to drop version {version} in application package {package_name}."
914
- )
915
-
916
- # If user did not provide --force, ask for confirmation
917
- user_prompt = (
918
- f"Are you sure you want to drop version {version} "
919
- f"in application package {package_name}? "
920
- f"Once dropped, this operation cannot be undone."
921
- )
922
- if not policy.should_proceed(user_prompt):
923
- if interactive:
924
- console.message("Not dropping version.")
925
- raise typer.Exit(0)
926
- else:
927
- console.message(
928
- "Cannot drop version non-interactively without --force."
929
- )
930
- raise typer.Exit(1)
931
-
932
- # Drop the version
933
- sql_executor = get_sql_executor()
934
- with sql_executor.use_role(package_role):
935
- try:
936
- sql_executor.execute_query(
937
- f"alter application package {package_name} drop version {version}"
938
- )
939
- except ProgrammingError as err:
940
- raise err # e.g. version is referenced in a release directive(s)
941
-
942
- console.message(
943
- f"Version {version} in application package {package_name} dropped successfully."
944
- )
945
-
946
- @staticmethod
947
- def get_existing_app_pkg_info(
948
- package_name: str,
949
- package_role: str,
950
- ) -> Optional[dict]:
756
+ def get_existing_app_pkg_info(self) -> Optional[dict]:
951
757
  """
952
758
  Check for an existing application package by the same name as in project definition, in account.
953
759
  It executes a 'show application packages like' query and returns the result as single row, if one exists.
954
760
  """
955
761
  sql_executor = get_sql_executor()
956
- with sql_executor.use_role(package_role):
762
+ with sql_executor.use_role(self.role):
957
763
  return sql_executor.show_specific_object(
958
- "application packages", package_name, name_col=NAME_COL
764
+ "application packages", self.name, name_col=NAME_COL
959
765
  )
960
766
 
961
- @staticmethod
962
- def get_app_pkg_distribution_in_snowflake(
963
- package_name: str,
964
- package_role: str,
965
- ) -> str:
767
+ def get_app_pkg_distribution_in_snowflake(self) -> str:
966
768
  """
967
769
  Returns the 'distribution' attribute of a 'describe application package' SQL query, in lowercase.
968
770
  """
969
771
  sql_executor = get_sql_executor()
970
- with sql_executor.use_role(package_role):
772
+ with sql_executor.use_role(self.role):
971
773
  try:
972
774
  desc_cursor = sql_executor.execute_query(
973
- f"describe application package {package_name}"
775
+ f"describe application package {self.name}"
974
776
  )
975
777
  except ProgrammingError as err:
976
778
  generic_sql_error_handler(err)
@@ -984,36 +786,31 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
984
786
  raise ObjectPropertyNotFoundError(
985
787
  property_name="distribution",
986
788
  object_type="application package",
987
- object_name=package_name,
789
+ object_name=self.name,
988
790
  )
989
791
 
990
- @classmethod
991
792
  def verify_project_distribution(
992
- cls,
993
- console: AbstractConsole,
994
- package_name: str,
995
- package_role: str,
996
- package_distribution: str,
793
+ self,
997
794
  expected_distribution: Optional[str] = None,
998
795
  ) -> bool:
999
796
  """
1000
797
  Returns true if the 'distribution' attribute of an existing application package in snowflake
1001
798
  is the same as the the attribute specified in project definition file.
1002
799
  """
800
+ model = self._entity_model
801
+ workspace_ctx = self._workspace_ctx
802
+
1003
803
  actual_distribution = (
1004
804
  expected_distribution
1005
805
  if expected_distribution
1006
- else cls.get_app_pkg_distribution_in_snowflake(
1007
- package_name=package_name,
1008
- package_role=package_role,
1009
- )
806
+ else self.get_app_pkg_distribution_in_snowflake()
1010
807
  )
1011
- project_def_distribution = package_distribution.lower()
808
+ project_def_distribution = model.distribution.lower()
1012
809
  if actual_distribution != project_def_distribution:
1013
- console.warning(
810
+ workspace_ctx.console.warning(
1014
811
  dedent(
1015
812
  f"""\
1016
- Application package {package_name} in your Snowflake account has distribution property {actual_distribution},
813
+ Application package {self.name} in your Snowflake account has distribution property {actual_distribution},
1017
814
  which does not match the value specified in project definition file: {project_def_distribution}.
1018
815
  """
1019
816
  )
@@ -1021,57 +818,24 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1021
818
  return False
1022
819
  return True
1023
820
 
1024
- @staticmethod
1025
- @contextmanager
1026
- def use_package_warehouse(
1027
- package_warehouse: Optional[str],
1028
- ):
1029
- if package_warehouse:
1030
- with get_sql_executor().use_warehouse(package_warehouse):
1031
- yield
1032
- else:
1033
- raise ClickException(
1034
- dedent(
1035
- f"""\
1036
- Application package warehouse cannot be empty.
1037
- Please provide a value for it in your connection information or your project definition file.
1038
- """
1039
- )
1040
- )
1041
-
1042
- @classmethod
1043
- def create_app_package(
1044
- cls,
1045
- console: AbstractConsole,
1046
- package_name: str,
1047
- package_role: str,
1048
- package_distribution: str,
1049
- ) -> None:
821
+ def create_app_package(self) -> None:
1050
822
  """
1051
823
  Creates the application package with our up-to-date stage if none exists.
1052
824
  """
825
+ model = self._entity_model
826
+ console = self._workspace_ctx.console
1053
827
 
1054
- # 1. Check for existing existing application package
1055
- show_obj_row = cls.get_existing_app_pkg_info(
1056
- package_name=package_name,
1057
- package_role=package_role,
1058
- )
828
+ # 1. Check for existing application package
829
+ show_obj_row = self.get_existing_app_pkg_info()
1059
830
 
1060
831
  if show_obj_row:
1061
832
  # 2. Check distribution of the existing application package
1062
- actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
1063
- package_name=package_name,
1064
- package_role=package_role,
1065
- )
1066
- if not cls.verify_project_distribution(
1067
- console=console,
1068
- package_name=package_name,
1069
- package_role=package_role,
1070
- package_distribution=package_distribution,
1071
- expected_distribution=actual_distribution,
833
+ actual_distribution = self.get_app_pkg_distribution_in_snowflake()
834
+ if not self.verify_project_distribution(
835
+ expected_distribution=actual_distribution
1072
836
  ):
1073
837
  console.warning(
1074
- f"Continuing to execute `snow app run` on application package {package_name} with distribution '{actual_distribution}'."
838
+ f"Continuing to execute `snow app run` on application package {self.name} with distribution '{actual_distribution}'."
1075
839
  )
1076
840
 
1077
841
  # 3. If actual_distribution is external, skip comment check
@@ -1079,88 +843,47 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1079
843
  row_comment = show_obj_row[COMMENT_COL]
1080
844
 
1081
845
  if row_comment not in ALLOWED_SPECIAL_COMMENTS:
1082
- raise ApplicationPackageAlreadyExistsError(package_name)
846
+ raise ApplicationPackageAlreadyExistsError(self.name)
1083
847
 
1084
848
  return
1085
849
 
1086
850
  # If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
1087
851
  sql_executor = get_sql_executor()
1088
- with sql_executor.use_role(package_role):
1089
- console.step(f"Creating new application package {package_name} in account.")
852
+ with sql_executor.use_role(self.role):
853
+ console.step(f"Creating new application package {self.name} in account.")
1090
854
  sql_executor.execute_query(
1091
855
  dedent(
1092
856
  f"""\
1093
- create application package {package_name}
857
+ create application package {self.name}
1094
858
  comment = {SPECIAL_COMMENT}
1095
- distribution = {package_distribution}
859
+ distribution = {model.distribution}
1096
860
  """
1097
861
  )
1098
862
  )
1099
863
 
1100
- @classmethod
1101
- def execute_post_deploy_hooks(
1102
- cls,
1103
- console: AbstractConsole,
1104
- project_root: Path,
1105
- post_deploy_hooks: Optional[List[PostDeployHook]],
1106
- package_name: str,
1107
- package_warehouse: Optional[str],
1108
- ):
1109
- get_cli_context().metrics.set_counter_default(
1110
- CLICounterField.POST_DEPLOY_SCRIPTS, 0
864
+ def execute_post_deploy_hooks(self):
865
+ execute_post_deploy_hooks(
866
+ console=self._workspace_ctx.console,
867
+ project_root=self.project_root,
868
+ post_deploy_hooks=self.post_deploy_hooks,
869
+ deployed_object_type="application package",
870
+ role_name=self.role,
871
+ warehouse_name=self.warehouse,
872
+ database_name=self.name,
1111
873
  )
1112
874
 
1113
- if post_deploy_hooks:
1114
- with cls.use_package_warehouse(package_warehouse):
1115
- execute_post_deploy_hooks(
1116
- console=console,
1117
- project_root=project_root,
1118
- post_deploy_hooks=post_deploy_hooks,
1119
- deployed_object_type="application package",
1120
- database_name=package_name,
1121
- )
1122
-
1123
- @classmethod
1124
875
  def validate_setup_script(
1125
- cls,
1126
- console: AbstractConsole,
1127
- project_root: Path,
1128
- deploy_root: Path,
1129
- bundle_root: Path,
1130
- generated_root: Path,
1131
- artifacts: list[PathMapping],
1132
- package_name: str,
1133
- package_role: str,
1134
- package_distribution: str,
1135
- package_warehouse: str | None,
1136
- prune: bool,
1137
- recursive: bool,
1138
- paths: List[Path] | None,
1139
- stage_fqn: str,
1140
- policy: PolicyBase,
1141
- use_scratch_stage: bool,
1142
- scratch_stage_fqn: str,
876
+ self, use_scratch_stage: bool, interactive: bool, force: bool
1143
877
  ):
878
+ workspace_ctx = self._workspace_ctx
879
+ console = workspace_ctx.console
880
+
1144
881
  """Validates Native App setup script SQL."""
1145
882
  with console.phase(f"Validating Snowflake Native App setup script."):
1146
- validation_result = cls.get_validation_result_static(
1147
- console=console,
1148
- project_root=project_root,
1149
- deploy_root=deploy_root,
1150
- bundle_root=bundle_root,
1151
- generated_root=generated_root,
1152
- artifacts=artifacts,
1153
- package_name=package_name,
1154
- package_role=package_role,
1155
- package_distribution=package_distribution,
1156
- prune=prune,
1157
- recursive=recursive,
1158
- paths=paths,
1159
- stage_fqn=stage_fqn,
1160
- package_warehouse=package_warehouse,
1161
- policy=policy,
883
+ validation_result = self.get_validation_result(
1162
884
  use_scratch_stage=use_scratch_stage,
1163
- scratch_stage_fqn=scratch_stage_fqn,
885
+ force=force,
886
+ interactive=interactive,
1164
887
  )
1165
888
 
1166
889
  # First print warnings, regardless of the outcome of validation
@@ -1177,78 +900,24 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1177
900
  if validation_result["status"] == "FAIL":
1178
901
  raise SetupScriptFailedValidation()
1179
902
 
1180
- def get_validation_result(self, use_scratch_stage: bool = True):
1181
- model = self._entity_model
1182
- workspace_ctx = self._workspace_ctx
1183
- package_name = model.fqn.identifier
1184
- return self.get_validation_result_static(
1185
- console=workspace_ctx.console,
1186
- project_root=workspace_ctx.project_root,
1187
- deploy_root=workspace_ctx.project_root / model.deploy_root,
1188
- bundle_root=workspace_ctx.project_root / model.bundle_root,
1189
- generated_root=(
1190
- workspace_ctx.project_root / model.deploy_root / model.generated_root
1191
- ),
1192
- artifacts=model.artifacts,
1193
- package_name=package_name,
1194
- package_role=(model.meta and model.meta.role) or workspace_ctx.default_role,
1195
- package_distribution=model.distribution,
1196
- prune=True,
1197
- recursive=True,
1198
- paths=[],
1199
- stage_fqn=f"{package_name}.{model.stage}",
1200
- package_warehouse=(
1201
- (model.meta and model.meta.warehouse) or workspace_ctx.default_warehouse
1202
- ),
1203
- policy=AllowAlwaysPolicy(),
1204
- use_scratch_stage=use_scratch_stage,
1205
- scratch_stage_fqn=f"{package_name}.{model.scratch_stage}",
1206
- )
1207
-
1208
- @classmethod
1209
- def get_validation_result_static(
1210
- cls,
1211
- console: AbstractConsole,
1212
- project_root: Path,
1213
- deploy_root: Path,
1214
- bundle_root: Path,
1215
- generated_root: Path,
1216
- artifacts: list[PathMapping],
1217
- package_name: str,
1218
- package_role: str,
1219
- package_distribution: str,
1220
- package_warehouse: str | None,
1221
- prune: bool,
1222
- recursive: bool,
1223
- paths: List[Path] | None,
1224
- stage_fqn: str,
1225
- policy: PolicyBase,
1226
- use_scratch_stage: bool,
1227
- scratch_stage_fqn: str,
903
+ def get_validation_result(
904
+ self, use_scratch_stage: bool, interactive: bool, force: bool
1228
905
  ):
1229
906
  """Call system$validate_native_app_setup() to validate deployed Native App setup script."""
907
+ stage_fqn = self.stage_fqn
1230
908
  if use_scratch_stage:
1231
- stage_fqn = scratch_stage_fqn
1232
- cls.deploy(
1233
- console=console,
1234
- project_root=project_root,
1235
- deploy_root=deploy_root,
1236
- bundle_root=bundle_root,
1237
- generated_root=generated_root,
1238
- artifacts=artifacts,
909
+ stage_fqn = self.scratch_stage_fqn
910
+ self._deploy(
1239
911
  bundle_map=None,
1240
- package_name=package_name,
1241
- package_role=package_role,
1242
- package_distribution=package_distribution,
1243
- prune=prune,
1244
- recursive=recursive,
1245
- paths=paths,
912
+ prune=True,
913
+ recursive=True,
914
+ paths=[],
1246
915
  print_diff=False,
1247
916
  validate=False,
1248
- stage_fqn=stage_fqn,
1249
- package_warehouse=package_warehouse,
1250
- post_deploy_hooks=[],
1251
- policy=policy,
917
+ stage_fqn=self.scratch_stage_fqn,
918
+ interactive=interactive,
919
+ force=force,
920
+ run_post_deploy_hooks=False,
1252
921
  )
1253
922
  prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
1254
923
  sql_executor = get_sql_executor()
@@ -1258,7 +927,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1258
927
  )
1259
928
  except ProgrammingError as err:
1260
929
  if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
1261
- raise ApplicationPackageDoesNotExistError(package_name)
930
+ raise ApplicationPackageDoesNotExistError(self.name)
1262
931
  generic_sql_error_handler(err)
1263
932
  else:
1264
933
  if not cursor.rowcount:
@@ -1266,103 +935,112 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1266
935
  return json.loads(cursor.fetchone()[0])
1267
936
  finally:
1268
937
  if use_scratch_stage:
1269
- console.step(f"Dropping stage {scratch_stage_fqn}.")
1270
- with sql_executor.use_role(package_role):
938
+ self._workspace_ctx.console.step(
939
+ f"Dropping stage {self.scratch_stage_fqn}."
940
+ )
941
+ with sql_executor.use_role(self.role):
1271
942
  sql_executor.execute_query(
1272
- f"drop stage if exists {scratch_stage_fqn}"
943
+ f"drop stage if exists {self.scratch_stage_fqn}"
1273
944
  )
1274
945
 
1275
- @classmethod
1276
- def drop(
1277
- cls,
1278
- console: AbstractConsole,
1279
- package_name: str,
1280
- package_role: str,
1281
- force_drop: bool,
946
+ def resolve_version_info(
947
+ self,
948
+ version: str | None,
949
+ patch: int | None,
950
+ label: str | None,
951
+ bundle_map: BundleMap | None,
952
+ policy: PolicyBase,
953
+ interactive: bool,
1282
954
  ):
1283
- sql_executor = get_sql_executor()
1284
- needs_confirm = True
1285
-
1286
- # 1. If existing application package is not found, exit gracefully
1287
- show_obj_row = cls.get_existing_app_pkg_info(
1288
- package_name=package_name,
1289
- package_role=package_role,
1290
- )
1291
- if show_obj_row is None:
1292
- console.warning(
1293
- f"Role {package_role} does not own any application package with the name {package_name}, or the application package does not exist."
1294
- )
1295
- return
1296
-
1297
- with sql_executor.use_role(package_role):
1298
- # 2. Check for versions in the application package
1299
- show_versions_query = f"show versions in application package {package_name}"
1300
- show_versions_cursor = sql_executor.execute_query(
1301
- show_versions_query, cursor_class=DictCursor
1302
- )
1303
- if show_versions_cursor.rowcount is None:
1304
- raise SnowflakeSQLExecutionError(show_versions_query)
955
+ """Determine version name, patch number, and label from CLI provided values and manifest.yml version entry.
956
+ @param [Optional] version: version name as specified in the command
957
+ @param [Optional] patch: patch number as specified in the command
958
+ @param [Optional] label: version/patch label as specified in the command
959
+ @param [Optional] bundle_map: bundle_map if a deploy_root is prepared. _bundle() is performed otherwise.
960
+ @param policy: CLI policy
961
+ @param interactive: True if command is run in interactive mode, otherwise False
962
+ """
963
+ console = self._workspace_ctx.console
1305
964
 
1306
- if show_versions_cursor.rowcount > 0:
1307
- # allow dropping a package with versions when --force is set
1308
- if not force_drop:
1309
- raise CouldNotDropApplicationPackageWithVersions(
1310
- "Drop versions first, or use --force to override."
1311
- )
965
+ resolved_version = None
966
+ resolved_patch = None
967
+ resolved_label = ""
1312
968
 
1313
- # 3. Check distribution of the existing application package
1314
- actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
1315
- package_name=package_name,
1316
- package_role=package_role,
1317
- )
1318
- if not cls.verify_project_distribution(
1319
- console=console,
1320
- package_name=package_name,
1321
- package_role=package_role,
1322
- package_distribution=actual_distribution,
1323
- ):
1324
- console.warning(
1325
- f"Dropping application package {package_name} with distribution '{actual_distribution}'."
969
+ # 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
+ if version is not None:
971
+ console.message(
972
+ "Ignoring version information from the application manifest since a version was explicitly specified with the command."
1326
973
  )
974
+ resolved_patch = patch
975
+ resolved_label = label if label is not None else ""
976
+ resolved_version = version
1327
977
 
1328
- # 4. If distribution is internal, check if created by the Snowflake CLI
1329
- row_comment = show_obj_row[COMMENT_COL]
1330
- if actual_distribution == INTERNAL_DISTRIBUTION:
1331
- if row_comment in ALLOWED_SPECIAL_COMMENTS:
1332
- needs_confirm = False
1333
- else:
1334
- if needs_confirmation(needs_confirm, force_drop):
1335
- console.warning(
1336
- f"Application package {package_name} was not created by Snowflake CLI."
1337
- )
978
+ # When version is not set by CLI, version name is read from manifest.yml. patch and label from CLI will be used, if provided.
1338
979
  else:
1339
- if needs_confirmation(needs_confirm, force_drop):
1340
- console.warning(
1341
- f"Application package {package_name} in your Snowflake account has distribution property '{EXTERNAL_DISTRIBUTION}' and could be associated with one or more of your listings on Snowflake Marketplace."
1342
- )
1343
-
1344
- if needs_confirmation(needs_confirm, force_drop):
1345
- should_drop_object = typer.confirm(
980
+ console.message(
1346
981
  dedent(
1347
982
  f"""\
1348
- Application package details:
1349
- Name: {package_name}
1350
- Created on: {show_obj_row["created_on"]}
1351
- Distribution: {actual_distribution}
1352
- Owner: {show_obj_row[OWNER_COL]}
1353
- Comment: {show_obj_row[COMMENT_COL]}
1354
- Are you sure you want to drop it?
983
+ Version was not provided through the Snowflake CLI. Checking version in the manifest.yml instead.
1355
984
  """
1356
985
  )
1357
986
  )
1358
- if not should_drop_object:
1359
- console.message(f"Did not drop application package {package_name}.")
1360
- return # The user desires to keep the application package, therefore exit gracefully
987
+ if bundle_map is None:
988
+ self._bundle()
989
+ (
990
+ resolved_version,
991
+ patch_manifest,
992
+ label_manifest,
993
+ ) = find_version_info_in_manifest_file(self.deploy_root)
994
+ if resolved_version is None:
995
+ raise ClickException(
996
+ "Manifest.yml file does not contain a value for the version field."
997
+ )
1361
998
 
1362
- # All validations have passed, drop object
1363
- drop_generic_object(
1364
- console=console,
1365
- object_type="application package",
1366
- object_name=package_name,
1367
- role=package_role,
999
+ # If patch is set in CLI and is also present in manifest.yml with different value, confirmation from
1000
+ # user is required to ignore patch from manifest.yml and proceed with CLI value.
1001
+ if (
1002
+ patch is not None
1003
+ and patch_manifest is not None
1004
+ and patch_manifest != patch
1005
+ ):
1006
+ console.warning(
1007
+ f"Cannot resolve version. Found patch: {patch_manifest} in manifest.yml which is different from provided patch {patch}."
1008
+ )
1009
+ user_prompt = f"Do you want to ignore patch in manifest.yml and proceed with provided --patch {patch}?"
1010
+ if not policy.should_proceed(user_prompt):
1011
+ if interactive:
1012
+ console.message("Not creating a new patch.")
1013
+ raise typer.Exit(0)
1014
+ else:
1015
+ console.message(
1016
+ "Could not create a new patch non-interactively without --force."
1017
+ )
1018
+ raise typer.Exit(1)
1019
+ resolved_patch = patch
1020
+ elif patch is not None:
1021
+ resolved_patch = patch
1022
+ else:
1023
+ resolved_patch = patch_manifest
1024
+
1025
+ # If label is not specified in CLI, label from manifest.yml is used. Even if patch is from CLI.
1026
+ resolved_label = label if label is not None else label_manifest
1027
+
1028
+ # 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:
1030
+ try:
1031
+ if not self.get_existing_version_info(resolved_version):
1032
+ raise BadOptionUsage(
1033
+ 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.",
1035
+ )
1036
+ except ApplicationPackageDoesNotExistError as app_err:
1037
+ raise BadOptionUsage(
1038
+ 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.",
1040
+ )
1041
+
1042
+ return VersionInfo(
1043
+ version_name=resolved_version,
1044
+ patch_number=resolved_patch,
1045
+ label=resolved_label,
1368
1046
  )