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