snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc2__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 (66) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/snow_connector.py +91 -37
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/connection/commands.py +3 -2
  7. snowflake/cli/_plugins/git/commands.py +55 -14
  8. snowflake/cli/_plugins/git/manager.py +14 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  10. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  11. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  12. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
  13. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
  14. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  15. snowflake/cli/_plugins/nativeapp/manager.py +74 -144
  16. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  17. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  18. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  19. snowflake/cli/_plugins/nativeapp/teardown_processor.py +17 -246
  20. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  21. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  22. snowflake/cli/_plugins/snowpark/common.py +17 -1
  23. snowflake/cli/_plugins/snowpark/models.py +2 -1
  24. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  25. snowflake/cli/_plugins/sql/commands.py +1 -2
  26. snowflake/cli/_plugins/stage/commands.py +2 -2
  27. snowflake/cli/_plugins/stage/manager.py +46 -15
  28. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  29. snowflake/cli/_plugins/streamlit/manager.py +13 -0
  30. snowflake/cli/_plugins/workspace/action_context.py +7 -0
  31. snowflake/cli/_plugins/workspace/commands.py +145 -32
  32. snowflake/cli/_plugins/workspace/manager.py +21 -4
  33. snowflake/cli/api/cli_global_context.py +136 -313
  34. snowflake/cli/api/commands/decorators.py +1 -1
  35. snowflake/cli/api/commands/flags.py +106 -102
  36. snowflake/cli/api/commands/snow_typer.py +15 -6
  37. snowflake/cli/api/config.py +18 -5
  38. snowflake/cli/api/connections.py +214 -0
  39. snowflake/cli/api/console/abc.py +4 -2
  40. snowflake/cli/api/constants.py +11 -0
  41. snowflake/cli/api/entities/application_entity.py +687 -2
  42. snowflake/cli/api/entities/application_package_entity.py +407 -9
  43. snowflake/cli/api/entities/common.py +7 -2
  44. snowflake/cli/api/entities/utils.py +80 -20
  45. snowflake/cli/api/exceptions.py +12 -2
  46. snowflake/cli/api/feature_flags.py +0 -2
  47. snowflake/cli/api/identifiers.py +3 -0
  48. snowflake/cli/api/project/definition.py +35 -1
  49. snowflake/cli/api/project/definition_conversion.py +352 -0
  50. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  51. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  52. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  53. snowflake/cli/api/project/schemas/project_definition.py +102 -43
  54. snowflake/cli/api/rendering/jinja.py +2 -16
  55. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  56. snowflake/cli/api/rendering/sql_templates.py +14 -4
  57. snowflake/cli/api/secure_path.py +13 -18
  58. snowflake/cli/api/secure_utils.py +90 -1
  59. snowflake/cli/api/sql_execution.py +13 -0
  60. snowflake/cli/api/utils/definition_rendering.py +7 -7
  61. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
  62. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
  63. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  64. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  65. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  66. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,10 @@
1
+ import json
1
2
  from contextlib import contextmanager
2
3
  from pathlib import Path
3
4
  from textwrap import dedent
4
- from typing import List, Optional
5
+ from typing import Callable, List, Optional
5
6
 
7
+ import typer
6
8
  from click import ClickException
7
9
  from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
8
10
  from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
@@ -10,29 +12,50 @@ from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler
10
12
  from snowflake.cli._plugins.nativeapp.constants import (
11
13
  ALLOWED_SPECIAL_COMMENTS,
12
14
  COMMENT_COL,
15
+ EXTERNAL_DISTRIBUTION,
13
16
  INTERNAL_DISTRIBUTION,
14
17
  NAME_COL,
18
+ OWNER_COL,
15
19
  SPECIAL_COMMENT,
16
20
  )
17
21
  from snowflake.cli._plugins.nativeapp.exceptions import (
18
22
  ApplicationPackageAlreadyExistsError,
23
+ ApplicationPackageDoesNotExistError,
24
+ CouldNotDropApplicationPackageWithVersions,
25
+ SetupScriptFailedValidation,
19
26
  )
27
+ from snowflake.cli._plugins.nativeapp.utils import (
28
+ needs_confirmation,
29
+ )
30
+ from snowflake.cli._plugins.stage.diff import DiffResult
31
+ from snowflake.cli._plugins.stage.manager import StageManager
20
32
  from snowflake.cli._plugins.workspace.action_context import ActionContext
21
33
  from snowflake.cli.api.console.abc import AbstractConsole
22
34
  from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
23
35
  from snowflake.cli.api.entities.utils import (
36
+ drop_generic_object,
24
37
  ensure_correct_owner,
38
+ execute_post_deploy_hooks,
25
39
  generic_sql_error_handler,
26
40
  render_script_templates,
41
+ sync_deploy_root_with_stage,
42
+ validation_item_to_str,
43
+ )
44
+ from snowflake.cli.api.errno import (
45
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
27
46
  )
28
47
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
29
48
  from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
30
49
  ApplicationPackageEntityModel,
31
50
  )
51
+ from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
52
+ from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
53
+ from snowflake.cli.api.project.util import extract_schema
32
54
  from snowflake.cli.api.rendering.jinja import (
33
- jinja_render_from_str,
55
+ get_basic_jinja_env,
34
56
  )
35
57
  from snowflake.connector import ProgrammingError
58
+ from snowflake.connector.cursor import DictCursor
36
59
 
37
60
 
38
61
  class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
@@ -40,23 +63,209 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
40
63
  A Native App application package.
41
64
  """
42
65
 
43
- def action_bundle(self, ctx: ActionContext):
66
+ def action_bundle(self, ctx: ActionContext, *args, **kwargs):
44
67
  model = self._entity_model
45
- bundle_map = build_bundle(
46
- ctx.project_root, Path(model.deploy_root), model.artifacts
47
- )
48
- bundle_context = BundleContext(
68
+ return self.bundle(
69
+ project_root=ctx.project_root,
70
+ deploy_root=Path(model.deploy_root),
71
+ bundle_root=Path(model.bundle_root),
72
+ generated_root=Path(model.generated_root),
49
73
  package_name=model.identifier,
50
74
  artifacts=model.artifacts,
75
+ )
76
+
77
+ def action_deploy(
78
+ self,
79
+ ctx: ActionContext,
80
+ prune: bool,
81
+ recursive: bool,
82
+ paths: List[Path],
83
+ validate: bool,
84
+ stage_fqn: Optional[str] = None,
85
+ *args,
86
+ **kwargs,
87
+ ):
88
+ model = self._entity_model
89
+ package_name = model.fqn.identifier
90
+ return self.deploy(
91
+ console=ctx.console,
51
92
  project_root=ctx.project_root,
52
- bundle_root=Path(model.bundle_root),
53
93
  deploy_root=Path(model.deploy_root),
94
+ bundle_root=Path(model.bundle_root),
54
95
  generated_root=Path(model.generated_root),
96
+ artifacts=model.artifacts,
97
+ package_name=package_name,
98
+ package_role=(model.meta and model.meta.role) or ctx.default_role,
99
+ package_distribution=model.distribution,
100
+ prune=prune,
101
+ recursive=recursive,
102
+ paths=paths,
103
+ print_diff=True,
104
+ validate=validate,
105
+ stage_fqn=stage_fqn or f"{package_name}.{model.stage}",
106
+ package_warehouse=(
107
+ (model.meta and model.meta.warehouse) or ctx.default_warehouse
108
+ ),
109
+ post_deploy_hooks=model.meta and model.meta.post_deploy,
110
+ package_scripts=[], # Package scripts are not supported in PDFv2
111
+ )
112
+
113
+ def action_drop(self, ctx: ActionContext, force_drop: bool, *args, **kwargs):
114
+ model = self._entity_model
115
+ package_name = model.fqn.identifier
116
+ if model.meta and model.meta.role:
117
+ package_role = model.meta.role
118
+ else:
119
+ package_role = ctx.default_role
120
+
121
+ self.drop(
122
+ console=ctx.console,
123
+ package_name=package_name,
124
+ package_role=package_role,
125
+ force_drop=force_drop,
126
+ )
127
+
128
+ def action_validate(self, ctx: ActionContext, *args, **kwargs):
129
+ model = self._entity_model
130
+ package_name = model.fqn.identifier
131
+ stage_fqn = f"{package_name}.{model.stage}"
132
+ if model.meta and model.meta.role:
133
+ package_role = model.meta.role
134
+ else:
135
+ package_role = ctx.default_role
136
+
137
+ def deploy_to_scratch_stage_fn():
138
+ self.action_deploy(
139
+ ctx=ctx,
140
+ prune=True,
141
+ recursive=True,
142
+ paths=[],
143
+ validate=False,
144
+ stage_fqn=model.scratch_stage,
145
+ )
146
+
147
+ self.validate_setup_script(
148
+ console=ctx.console,
149
+ package_name=package_name,
150
+ package_role=package_role,
151
+ stage_fqn=stage_fqn,
152
+ use_scratch_stage=True,
153
+ scratch_stage_fqn=model.scratch_stage,
154
+ deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn,
155
+ )
156
+ ctx.console.message("Setup script is valid")
157
+
158
+ @staticmethod
159
+ def bundle(
160
+ project_root: Path,
161
+ deploy_root: Path,
162
+ bundle_root: Path,
163
+ generated_root: Path,
164
+ artifacts: list[PathMapping],
165
+ package_name: str,
166
+ ):
167
+ bundle_map = build_bundle(project_root, deploy_root, artifacts)
168
+ bundle_context = BundleContext(
169
+ package_name=package_name,
170
+ artifacts=artifacts,
171
+ project_root=project_root,
172
+ bundle_root=bundle_root,
173
+ deploy_root=deploy_root,
174
+ generated_root=generated_root,
55
175
  )
56
176
  compiler = NativeAppCompiler(bundle_context)
57
177
  compiler.compile_artifacts()
58
178
  return bundle_map
59
179
 
180
+ @classmethod
181
+ def deploy(
182
+ cls,
183
+ console: AbstractConsole,
184
+ project_root: Path,
185
+ deploy_root: Path,
186
+ bundle_root: Path,
187
+ generated_root: Path,
188
+ artifacts: list[PathMapping],
189
+ package_name: str,
190
+ package_role: str,
191
+ package_distribution: str,
192
+ package_warehouse: str | None,
193
+ prune: bool,
194
+ recursive: bool,
195
+ paths: List[Path],
196
+ print_diff: bool,
197
+ validate: bool,
198
+ stage_fqn: str,
199
+ post_deploy_hooks: list[PostDeployHook] | None,
200
+ package_scripts: List[str],
201
+ ) -> DiffResult:
202
+ # 1. Create a bundle
203
+ bundle_map = cls.bundle(
204
+ project_root=project_root,
205
+ deploy_root=deploy_root,
206
+ bundle_root=bundle_root,
207
+ generated_root=generated_root,
208
+ artifacts=artifacts,
209
+ package_name=package_name,
210
+ )
211
+
212
+ # 2. Create an empty application package, if none exists
213
+ cls.create_app_package(
214
+ console=console,
215
+ package_name=package_name,
216
+ package_role=package_role,
217
+ package_distribution=package_distribution,
218
+ )
219
+
220
+ with get_sql_executor().use_role(package_role):
221
+ if package_scripts:
222
+ cls.apply_package_scripts(
223
+ console=console,
224
+ package_scripts=package_scripts,
225
+ package_warehouse=package_warehouse,
226
+ project_root=project_root,
227
+ package_role=package_role,
228
+ package_name=package_name,
229
+ )
230
+
231
+ # 3. Upload files from deploy root local folder to the above stage
232
+ stage_schema = extract_schema(stage_fqn)
233
+ diff = sync_deploy_root_with_stage(
234
+ console=console,
235
+ deploy_root=deploy_root,
236
+ package_name=package_name,
237
+ stage_schema=stage_schema,
238
+ bundle_map=bundle_map,
239
+ role=package_role,
240
+ prune=prune,
241
+ recursive=recursive,
242
+ stage_fqn=stage_fqn,
243
+ local_paths_to_sync=paths,
244
+ print_diff=print_diff,
245
+ )
246
+
247
+ if post_deploy_hooks:
248
+ cls.execute_post_deploy_hooks(
249
+ console=console,
250
+ project_root=project_root,
251
+ post_deploy_hooks=post_deploy_hooks,
252
+ package_name=package_name,
253
+ package_warehouse=package_warehouse,
254
+ )
255
+
256
+ if validate:
257
+ cls.validate_setup_script(
258
+ console=console,
259
+ package_name=package_name,
260
+ package_role=package_role,
261
+ stage_fqn=stage_fqn,
262
+ use_scratch_stage=False,
263
+ scratch_stage_fqn="",
264
+ deploy_to_scratch_stage_fn=lambda *args: None,
265
+ )
266
+
267
+ return diff
268
+
60
269
  @staticmethod
61
270
  def get_existing_app_pkg_info(
62
271
  package_name: str,
@@ -178,9 +387,9 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
178
387
 
179
388
  queued_queries = render_script_templates(
180
389
  project_root,
181
- jinja_render_from_str,
182
390
  dict(package_name=package_name),
183
391
  package_scripts,
392
+ get_basic_jinja_env(),
184
393
  )
185
394
 
186
395
  # once we're sure all the templates expanded correctly, execute all of them
@@ -258,3 +467,192 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
258
467
  """
259
468
  )
260
469
  )
470
+
471
+ @classmethod
472
+ def execute_post_deploy_hooks(
473
+ cls,
474
+ console: AbstractConsole,
475
+ project_root: Path,
476
+ post_deploy_hooks: Optional[List[PostDeployHook]],
477
+ package_name: str,
478
+ package_warehouse: Optional[str],
479
+ ):
480
+ with cls.use_package_warehouse(package_warehouse):
481
+ execute_post_deploy_hooks(
482
+ console=console,
483
+ project_root=project_root,
484
+ post_deploy_hooks=post_deploy_hooks,
485
+ deployed_object_type="application package",
486
+ database_name=package_name,
487
+ )
488
+
489
+ @classmethod
490
+ def validate_setup_script(
491
+ cls,
492
+ console: AbstractConsole,
493
+ package_name: str,
494
+ package_role: str,
495
+ stage_fqn: str,
496
+ use_scratch_stage: bool,
497
+ scratch_stage_fqn: str,
498
+ deploy_to_scratch_stage_fn: Callable,
499
+ ):
500
+ """Validates Native App setup script SQL."""
501
+ with console.phase(f"Validating Snowflake Native App setup script."):
502
+ validation_result = cls.get_validation_result(
503
+ console=console,
504
+ package_name=package_name,
505
+ package_role=package_role,
506
+ stage_fqn=stage_fqn,
507
+ use_scratch_stage=use_scratch_stage,
508
+ scratch_stage_fqn=scratch_stage_fqn,
509
+ deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn,
510
+ )
511
+
512
+ # First print warnings, regardless of the outcome of validation
513
+ for warning in validation_result.get("warnings", []):
514
+ console.warning(validation_item_to_str(warning))
515
+
516
+ # Then print errors
517
+ for error in validation_result.get("errors", []):
518
+ # Print them as warnings for now since we're going to be
519
+ # revamping CLI output soon
520
+ console.warning(validation_item_to_str(error))
521
+
522
+ # Then raise an exception if validation failed
523
+ if validation_result["status"] == "FAIL":
524
+ raise SetupScriptFailedValidation()
525
+
526
+ @staticmethod
527
+ def get_validation_result(
528
+ console: AbstractConsole,
529
+ package_name: str,
530
+ package_role: str,
531
+ stage_fqn: str,
532
+ use_scratch_stage: bool,
533
+ scratch_stage_fqn: str,
534
+ deploy_to_scratch_stage_fn: Callable,
535
+ ):
536
+ """Call system$validate_native_app_setup() to validate deployed Native App setup script."""
537
+ if use_scratch_stage:
538
+ stage_fqn = scratch_stage_fqn
539
+ deploy_to_scratch_stage_fn()
540
+ prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
541
+ sql_executor = get_sql_executor()
542
+ try:
543
+ cursor = sql_executor.execute_query(
544
+ f"call system$validate_native_app_setup('{prefixed_stage_fqn}')"
545
+ )
546
+ except ProgrammingError as err:
547
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
548
+ raise ApplicationPackageDoesNotExistError(package_name)
549
+ generic_sql_error_handler(err)
550
+ else:
551
+ if not cursor.rowcount:
552
+ raise SnowflakeSQLExecutionError()
553
+ return json.loads(cursor.fetchone()[0])
554
+ finally:
555
+ if use_scratch_stage:
556
+ console.step(f"Dropping stage {scratch_stage_fqn}.")
557
+ with sql_executor.use_role(package_role):
558
+ sql_executor.execute_query(
559
+ f"drop stage if exists {scratch_stage_fqn}"
560
+ )
561
+
562
+ @classmethod
563
+ def drop(
564
+ cls,
565
+ console: AbstractConsole,
566
+ package_name: str,
567
+ package_role: str,
568
+ force_drop: bool,
569
+ ):
570
+ sql_executor = get_sql_executor()
571
+ needs_confirm = True
572
+
573
+ # 1. If existing application package is not found, exit gracefully
574
+ show_obj_row = cls.get_existing_app_pkg_info(
575
+ package_name=package_name,
576
+ package_role=package_role,
577
+ )
578
+ if show_obj_row is None:
579
+ console.warning(
580
+ f"Role {package_role} does not own any application package with the name {package_name}, or the application package does not exist."
581
+ )
582
+ return
583
+
584
+ # 2. Check for the right owner
585
+ ensure_correct_owner(row=show_obj_row, role=package_role, obj_name=package_name)
586
+
587
+ with sql_executor.use_role(package_role):
588
+ # 3. Check for versions in the application package
589
+ show_versions_query = f"show versions in application package {package_name}"
590
+ show_versions_cursor = sql_executor.execute_query(
591
+ show_versions_query, cursor_class=DictCursor
592
+ )
593
+ if show_versions_cursor.rowcount is None:
594
+ raise SnowflakeSQLExecutionError(show_versions_query)
595
+
596
+ if show_versions_cursor.rowcount > 0:
597
+ # allow dropping a package with versions when --force is set
598
+ if not force_drop:
599
+ raise CouldNotDropApplicationPackageWithVersions(
600
+ "Drop versions first, or use --force to override."
601
+ )
602
+
603
+ # 4. Check distribution of the existing application package
604
+ actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
605
+ package_name=package_name,
606
+ package_role=package_role,
607
+ )
608
+ if not cls.verify_project_distribution(
609
+ console=console,
610
+ package_name=package_name,
611
+ package_role=package_role,
612
+ package_distribution=actual_distribution,
613
+ ):
614
+ console.warning(
615
+ f"Dropping application package {package_name} with distribution '{actual_distribution}'."
616
+ )
617
+
618
+ # 5. If distribution is internal, check if created by the Snowflake CLI
619
+ row_comment = show_obj_row[COMMENT_COL]
620
+ if actual_distribution == INTERNAL_DISTRIBUTION:
621
+ if row_comment in ALLOWED_SPECIAL_COMMENTS:
622
+ needs_confirm = False
623
+ else:
624
+ if needs_confirmation(needs_confirm, force_drop):
625
+ console.warning(
626
+ f"Application package {package_name} was not created by Snowflake CLI."
627
+ )
628
+ else:
629
+ if needs_confirmation(needs_confirm, force_drop):
630
+ console.warning(
631
+ 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."
632
+ )
633
+
634
+ if needs_confirmation(needs_confirm, force_drop):
635
+ should_drop_object = typer.confirm(
636
+ dedent(
637
+ f"""\
638
+ Application package details:
639
+ Name: {package_name}
640
+ Created on: {show_obj_row["created_on"]}
641
+ Distribution: {actual_distribution}
642
+ Owner: {show_obj_row[OWNER_COL]}
643
+ Comment: {show_obj_row[COMMENT_COL]}
644
+ Are you sure you want to drop it?
645
+ """
646
+ )
647
+ )
648
+ if not should_drop_object:
649
+ console.message(f"Did not drop application package {package_name}.")
650
+ return # The user desires to keep the application package, therefore exit gracefully
651
+
652
+ # All validations have passed, drop object
653
+ drop_generic_object(
654
+ console=console,
655
+ object_type="application package",
656
+ object_name=package_name,
657
+ role=package_role,
658
+ )
@@ -7,6 +7,9 @@ from snowflake.cli.api.sql_execution import SqlExecutor
7
7
 
8
8
  class EntityActions(str, Enum):
9
9
  BUNDLE = "action_bundle"
10
+ DEPLOY = "action_deploy"
11
+ DROP = "action_drop"
12
+ VALIDATE = "action_validate"
10
13
 
11
14
 
12
15
  T = TypeVar("T")
@@ -35,11 +38,13 @@ class EntityBase(Generic[T]):
35
38
  """
36
39
  return callable(getattr(self, action, None))
37
40
 
38
- def perform(self, action: EntityActions, action_ctx: ActionContext):
41
+ def perform(
42
+ self, action: EntityActions, action_ctx: ActionContext, *args, **kwargs
43
+ ):
39
44
  """
40
45
  Performs the requested action.
41
46
  """
42
- return getattr(self, action)(action_ctx)
47
+ return getattr(self, action)(action_ctx, *args, **kwargs)
43
48
 
44
49
 
45
50
  def get_sql_executor() -> SqlExecutor:
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
  from textwrap import dedent
4
- from typing import Any, Callable, Dict, List, NoReturn, Optional
4
+ from typing import Any, List, NoReturn, Optional
5
5
 
6
6
  import jinja2
7
7
  from click import ClickException
@@ -11,7 +11,7 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
11
11
  )
12
12
  from snowflake.cli._plugins.nativeapp.constants import OWNER_COL
13
13
  from snowflake.cli._plugins.nativeapp.exceptions import (
14
- InvalidScriptError,
14
+ InvalidTemplateInFileError,
15
15
  MissingScriptError,
16
16
  UnexpectedOwnerError,
17
17
  )
@@ -25,19 +25,22 @@ from snowflake.cli._plugins.stage.diff import (
25
25
  to_stage_path,
26
26
  )
27
27
  from snowflake.cli._plugins.stage.utils import print_diff_to_console
28
+ from snowflake.cli.api.cli_global_context import get_cli_context
28
29
  from snowflake.cli.api.console.abc import AbstractConsole
29
30
  from snowflake.cli.api.entities.common import get_sql_executor
30
31
  from snowflake.cli.api.errno import (
31
32
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
32
33
  NO_WAREHOUSE_SELECTED_IN_SESSION,
33
34
  )
35
+ from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
34
36
  from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
35
37
  from snowflake.cli.api.project.util import unquote_identifier
36
38
  from snowflake.cli.api.rendering.sql_templates import (
37
- snowflake_sql_jinja_render,
39
+ choose_sql_jinja_env_based_on_template_syntax,
38
40
  )
39
41
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
40
42
  from snowflake.connector import ProgrammingError
43
+ from snowflake.connector.cursor import SnowflakeCursor
41
44
 
42
45
 
43
46
  def generic_sql_error_handler(
@@ -272,8 +275,7 @@ def execute_post_deploy_hooks(
272
275
 
273
276
  scripts_content_list = render_script_templates(
274
277
  project_root,
275
- snowflake_sql_jinja_render,
276
- {},
278
+ get_cli_context().template_context,
277
279
  sql_scripts_paths,
278
280
  )
279
281
 
@@ -287,35 +289,93 @@ def execute_post_deploy_hooks(
287
289
 
288
290
  def render_script_templates(
289
291
  project_root: Path,
290
- render_from_str: Callable[[str, Dict[str, Any]], str],
291
292
  jinja_context: dict[str, Any],
292
293
  scripts: List[str],
294
+ override_env: Optional[jinja2.Environment] = None,
293
295
  ) -> List[str]:
294
296
  """
295
297
  Input:
296
298
  - project_root: path to project root
297
- - render_from_str: function which renders a jinja template from a string and jinja context
298
299
  - jinja_context: a dictionary with the jinja context
299
300
  - scripts: list of script paths relative to the project root
301
+ - override_env: optional jinja environment to use for rendering,
302
+ if not provided, the environment will be chosen based on the template syntax
300
303
  Returns:
301
304
  - List of rendered scripts content
302
305
  Size of the return list is the same as the size of the input scripts list.
303
306
  """
304
- scripts_contents = []
305
- for relpath in scripts:
306
- script_full_path = SecurePath(project_root) / relpath
307
+ return [
308
+ render_script_template(project_root, jinja_context, script, override_env)
309
+ for script in scripts
310
+ ]
311
+
312
+
313
+ def render_script_template(
314
+ project_root: Path,
315
+ jinja_context: dict[str, Any],
316
+ script: str,
317
+ override_env: Optional[jinja2.Environment] = None,
318
+ ) -> str:
319
+ script_full_path = SecurePath(project_root) / script
320
+ try:
321
+ template_content = script_full_path.read_text(file_size_limit_mb=UNLIMITED)
322
+ env = override_env or choose_sql_jinja_env_based_on_template_syntax(
323
+ template_content, reference_name=script
324
+ )
325
+ return env.from_string(template_content).render(jinja_context)
326
+
327
+ except FileNotFoundError as e:
328
+ raise MissingScriptError(script) from e
329
+
330
+ except jinja2.TemplateSyntaxError as e:
331
+ raise InvalidTemplateInFileError(script, e, e.lineno) from e
332
+
333
+ except jinja2.UndefinedError as e:
334
+ raise InvalidTemplateInFileError(script, e) from e
335
+
336
+
337
+ def validation_item_to_str(item: dict[str, str | int]):
338
+ s = item["message"]
339
+ if item["errorCode"]:
340
+ s = f"{s} (error code {item['errorCode']})"
341
+ return s
342
+
343
+
344
+ def drop_generic_object(
345
+ console: AbstractConsole,
346
+ object_type: str,
347
+ object_name: str,
348
+ role: str,
349
+ cascade: bool = False,
350
+ ):
351
+ """
352
+ Drop object using the given role.
353
+ """
354
+ sql_executor = get_sql_executor()
355
+ with sql_executor.use_role(role):
356
+ console.step(f"Dropping {object_type} {object_name} now.")
357
+ drop_query = f"drop {object_type} {object_name}"
358
+ if cascade:
359
+ drop_query += " cascade"
307
360
  try:
308
- template_content = script_full_path.read_text(file_size_limit_mb=UNLIMITED)
309
- result = render_from_str(template_content, jinja_context)
310
- scripts_contents.append(result)
361
+ sql_executor.execute_query(drop_query)
362
+ except:
363
+ raise SnowflakeSQLExecutionError(drop_query)
311
364
 
312
- except FileNotFoundError as e:
313
- raise MissingScriptError(relpath) from e
365
+ console.message(f"Dropped {object_type} {object_name} successfully.")
314
366
 
315
- except jinja2.TemplateSyntaxError as e:
316
- raise InvalidScriptError(relpath, e, e.lineno) from e
317
367
 
318
- except jinja2.UndefinedError as e:
319
- raise InvalidScriptError(relpath, e) from e
368
+ def print_messages(
369
+ console: AbstractConsole, create_or_upgrade_cursor: Optional[SnowflakeCursor]
370
+ ):
371
+ """
372
+ Shows messages in the console returned by the CREATE or UPGRADE
373
+ APPLICATION command.
374
+ """
375
+ if not create_or_upgrade_cursor:
376
+ return
320
377
 
321
- return scripts_contents
378
+ messages = [row[0] for row in create_or_upgrade_cursor.fetchall()]
379
+ for message in messages:
380
+ console.warning(message)
381
+ console.message("")
@@ -19,6 +19,7 @@ from typing import Optional
19
19
 
20
20
  from click.exceptions import ClickException, UsageError
21
21
  from snowflake.cli.api.constants import ObjectType
22
+ from snowflake.connector.compat import IS_WINDOWS
22
23
 
23
24
 
24
25
  class EnvironmentVariableNotFoundError(ClickException):
@@ -140,9 +141,18 @@ class DirectoryIsNotEmptyError(ClickException):
140
141
 
141
142
  class ConfigFileTooWidePermissionsError(ClickException):
142
143
  def __init__(self, path: Path):
143
- super().__init__(
144
- f'Configuration file {path} has too wide permissions, run `chmod 0600 "{path}"`'
144
+ change_permissons_command = (
145
+ f'icacls "{path}" /deny <USER_ID>:F'
146
+ if IS_WINDOWS
147
+ else f'chmod 0600 "{path}"'
145
148
  )
149
+ msg = f"Configuration file {path} has too wide permissions, run `{change_permissons_command}`."
150
+ if IS_WINDOWS:
151
+ msg += (
152
+ f'\nTo check which users have access to the file run `icacls "{path}"`.'
153
+ "Run the above command for all users except you and administrators."
154
+ )
155
+ super().__init__(msg)
146
156
 
147
157
 
148
158
  class DatabaseNotProvidedError(ClickException):