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