snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc1__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 (51) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/snow_connector.py +18 -11
  3. snowflake/cli/_plugins/connection/commands.py +3 -2
  4. snowflake/cli/_plugins/git/manager.py +14 -6
  5. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  6. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  7. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  8. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
  10. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  11. snowflake/cli/_plugins/nativeapp/manager.py +29 -58
  12. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  13. snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
  14. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  15. snowflake/cli/_plugins/snowpark/common.py +17 -1
  16. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  17. snowflake/cli/_plugins/sql/commands.py +1 -2
  18. snowflake/cli/_plugins/stage/commands.py +2 -2
  19. snowflake/cli/_plugins/stage/manager.py +46 -15
  20. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  21. snowflake/cli/_plugins/streamlit/manager.py +4 -0
  22. snowflake/cli/_plugins/workspace/action_context.py +6 -0
  23. snowflake/cli/_plugins/workspace/commands.py +103 -22
  24. snowflake/cli/_plugins/workspace/manager.py +20 -4
  25. snowflake/cli/api/cli_global_context.py +6 -6
  26. snowflake/cli/api/commands/decorators.py +1 -1
  27. snowflake/cli/api/commands/flags.py +31 -12
  28. snowflake/cli/api/commands/snow_typer.py +9 -2
  29. snowflake/cli/api/config.py +17 -4
  30. snowflake/cli/api/constants.py +11 -0
  31. snowflake/cli/api/entities/application_package_entity.py +296 -3
  32. snowflake/cli/api/entities/common.py +6 -2
  33. snowflake/cli/api/entities/utils.py +46 -10
  34. snowflake/cli/api/exceptions.py +12 -2
  35. snowflake/cli/api/feature_flags.py +0 -2
  36. snowflake/cli/api/project/definition.py +24 -1
  37. snowflake/cli/api/project/definition_conversion.py +194 -0
  38. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  39. snowflake/cli/api/project/schemas/project_definition.py +1 -4
  40. snowflake/cli/api/rendering/jinja.py +2 -16
  41. snowflake/cli/api/rendering/project_definition_templates.py +1 -1
  42. snowflake/cli/api/rendering/sql_templates.py +7 -4
  43. snowflake/cli/api/secure_path.py +13 -18
  44. snowflake/cli/api/secure_utils.py +90 -1
  45. snowflake/cli/api/sql_execution.py +13 -0
  46. snowflake/cli/api/utils/definition_rendering.py +4 -6
  47. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
  48. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
  49. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
  50. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  51. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.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,48 @@ 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.manager import StageManager
20
31
  from snowflake.cli._plugins.workspace.action_context import ActionContext
21
32
  from snowflake.cli.api.console.abc import AbstractConsole
22
33
  from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
23
34
  from snowflake.cli.api.entities.utils import (
35
+ drop_generic_object,
24
36
  ensure_correct_owner,
37
+ execute_post_deploy_hooks,
25
38
  generic_sql_error_handler,
26
39
  render_script_templates,
40
+ sync_deploy_root_with_stage,
41
+ validation_item_to_str,
42
+ )
43
+ from snowflake.cli.api.errno import (
44
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
27
45
  )
28
46
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
29
47
  from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
30
48
  ApplicationPackageEntityModel,
31
49
  )
50
+ from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
51
+ from snowflake.cli.api.project.util import extract_schema
32
52
  from snowflake.cli.api.rendering.jinja import (
33
- jinja_render_from_str,
53
+ get_basic_jinja_env,
34
54
  )
35
55
  from snowflake.connector import ProgrammingError
56
+ from snowflake.connector.cursor import DictCursor
36
57
 
37
58
 
38
59
  class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
@@ -57,6 +78,89 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
57
78
  compiler.compile_artifacts()
58
79
  return bundle_map
59
80
 
81
+ def action_deploy(
82
+ self,
83
+ ctx: ActionContext,
84
+ prune: bool,
85
+ recursive: bool,
86
+ paths: List[Path],
87
+ validate: bool,
88
+ ):
89
+ model = self._entity_model
90
+ package_name = model.fqn.identifier
91
+ if model.meta and model.meta.role:
92
+ package_role = model.meta.role
93
+ else:
94
+ package_role = ctx.default_role
95
+
96
+ # 1. Create a bundle
97
+ bundle_map = self.action_bundle(ctx)
98
+
99
+ # 2. Create an empty application package, if none exists
100
+ self.create_app_package(
101
+ console=ctx.console,
102
+ package_name=package_name,
103
+ package_role=package_role,
104
+ package_distribution=model.distribution,
105
+ )
106
+
107
+ with get_sql_executor().use_role(package_role):
108
+ # 3. Upload files from deploy root local folder to the above stage
109
+ stage_fqn = f"{package_name}.{model.stage}"
110
+ stage_schema = extract_schema(stage_fqn)
111
+ sync_deploy_root_with_stage(
112
+ console=ctx.console,
113
+ deploy_root=Path(model.deploy_root),
114
+ package_name=package_name,
115
+ stage_schema=stage_schema,
116
+ bundle_map=bundle_map,
117
+ role=package_role,
118
+ prune=prune,
119
+ recursive=recursive,
120
+ stage_fqn=stage_fqn,
121
+ local_paths_to_sync=paths,
122
+ print_diff=True,
123
+ )
124
+
125
+ if model.meta and model.meta.post_deploy:
126
+ self.execute_post_deploy_hooks(
127
+ console=ctx.console,
128
+ project_root=ctx.project_root,
129
+ post_deploy_hooks=model.meta.post_deploy,
130
+ package_name=package_name,
131
+ package_warehouse=model.meta.warehouse or ctx.default_warehouse,
132
+ )
133
+
134
+ if validate:
135
+ self.validate_setup_script(
136
+ console=ctx.console,
137
+ package_name=package_name,
138
+ package_role=package_role,
139
+ stage_fqn=stage_fqn,
140
+ use_scratch_stage=False,
141
+ scratch_stage_fqn="",
142
+ deploy_to_scratch_stage_fn=lambda *args: None,
143
+ )
144
+
145
+ def action_drop(
146
+ self,
147
+ ctx: ActionContext,
148
+ force_drop: bool,
149
+ ):
150
+ model = self._entity_model
151
+ package_name = model.fqn.identifier
152
+ if model.meta and model.meta.role:
153
+ package_role = model.meta.role
154
+ else:
155
+ package_role = ctx.default_role
156
+
157
+ self.drop(
158
+ console=ctx.console,
159
+ package_name=package_name,
160
+ package_role=package_role,
161
+ force_drop=force_drop,
162
+ )
163
+
60
164
  @staticmethod
61
165
  def get_existing_app_pkg_info(
62
166
  package_name: str,
@@ -178,9 +282,9 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
178
282
 
179
283
  queued_queries = render_script_templates(
180
284
  project_root,
181
- jinja_render_from_str,
182
285
  dict(package_name=package_name),
183
286
  package_scripts,
287
+ get_basic_jinja_env(),
184
288
  )
185
289
 
186
290
  # once we're sure all the templates expanded correctly, execute all of them
@@ -258,3 +362,192 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
258
362
  """
259
363
  )
260
364
  )
365
+
366
+ @classmethod
367
+ def execute_post_deploy_hooks(
368
+ cls,
369
+ console: AbstractConsole,
370
+ project_root: Path,
371
+ post_deploy_hooks: Optional[List[PostDeployHook]],
372
+ package_name: str,
373
+ package_warehouse: Optional[str],
374
+ ):
375
+ with cls.use_package_warehouse(package_warehouse):
376
+ execute_post_deploy_hooks(
377
+ console=console,
378
+ project_root=project_root,
379
+ post_deploy_hooks=post_deploy_hooks,
380
+ deployed_object_type="application package",
381
+ database_name=package_name,
382
+ )
383
+
384
+ @classmethod
385
+ def validate_setup_script(
386
+ cls,
387
+ console: AbstractConsole,
388
+ package_name: str,
389
+ package_role: str,
390
+ stage_fqn: str,
391
+ use_scratch_stage: bool,
392
+ scratch_stage_fqn: str,
393
+ deploy_to_scratch_stage_fn: Callable,
394
+ ):
395
+ """Validates Native App setup script SQL."""
396
+ with console.phase(f"Validating Snowflake Native App setup script."):
397
+ validation_result = cls.get_validation_result(
398
+ console=console,
399
+ package_name=package_name,
400
+ package_role=package_role,
401
+ stage_fqn=stage_fqn,
402
+ use_scratch_stage=use_scratch_stage,
403
+ scratch_stage_fqn=scratch_stage_fqn,
404
+ deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn,
405
+ )
406
+
407
+ # First print warnings, regardless of the outcome of validation
408
+ for warning in validation_result.get("warnings", []):
409
+ console.warning(validation_item_to_str(warning))
410
+
411
+ # Then print errors
412
+ for error in validation_result.get("errors", []):
413
+ # Print them as warnings for now since we're going to be
414
+ # revamping CLI output soon
415
+ console.warning(validation_item_to_str(error))
416
+
417
+ # Then raise an exception if validation failed
418
+ if validation_result["status"] == "FAIL":
419
+ raise SetupScriptFailedValidation()
420
+
421
+ @staticmethod
422
+ def get_validation_result(
423
+ console: AbstractConsole,
424
+ package_name: str,
425
+ package_role: str,
426
+ stage_fqn: str,
427
+ use_scratch_stage: bool,
428
+ scratch_stage_fqn: str,
429
+ deploy_to_scratch_stage_fn: Callable,
430
+ ):
431
+ """Call system$validate_native_app_setup() to validate deployed Native App setup script."""
432
+ if use_scratch_stage:
433
+ stage_fqn = scratch_stage_fqn
434
+ deploy_to_scratch_stage_fn()
435
+ prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
436
+ sql_executor = get_sql_executor()
437
+ try:
438
+ cursor = sql_executor.execute_query(
439
+ f"call system$validate_native_app_setup('{prefixed_stage_fqn}')"
440
+ )
441
+ except ProgrammingError as err:
442
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
443
+ raise ApplicationPackageDoesNotExistError(package_name)
444
+ generic_sql_error_handler(err)
445
+ else:
446
+ if not cursor.rowcount:
447
+ raise SnowflakeSQLExecutionError()
448
+ return json.loads(cursor.fetchone()[0])
449
+ finally:
450
+ if use_scratch_stage:
451
+ console.step(f"Dropping stage {scratch_stage_fqn}.")
452
+ with sql_executor.use_role(package_role):
453
+ sql_executor.execute_query(
454
+ f"drop stage if exists {scratch_stage_fqn}"
455
+ )
456
+
457
+ @classmethod
458
+ def drop(
459
+ cls,
460
+ console: AbstractConsole,
461
+ package_name: str,
462
+ package_role: str,
463
+ force_drop: bool,
464
+ ):
465
+ sql_executor = get_sql_executor()
466
+ needs_confirm = True
467
+
468
+ # 1. If existing application package is not found, exit gracefully
469
+ show_obj_row = cls.get_existing_app_pkg_info(
470
+ package_name=package_name,
471
+ package_role=package_role,
472
+ )
473
+ if show_obj_row is None:
474
+ console.warning(
475
+ f"Role {package_role} does not own any application package with the name {package_name}, or the application package does not exist."
476
+ )
477
+ return
478
+
479
+ # 2. Check for the right owner
480
+ ensure_correct_owner(row=show_obj_row, role=package_role, obj_name=package_name)
481
+
482
+ with sql_executor.use_role(package_role):
483
+ # 3. Check for versions in the application package
484
+ show_versions_query = f"show versions in application package {package_name}"
485
+ show_versions_cursor = sql_executor.execute_query(
486
+ show_versions_query, cursor_class=DictCursor
487
+ )
488
+ if show_versions_cursor.rowcount is None:
489
+ raise SnowflakeSQLExecutionError(show_versions_query)
490
+
491
+ if show_versions_cursor.rowcount > 0:
492
+ # allow dropping a package with versions when --force is set
493
+ if not force_drop:
494
+ raise CouldNotDropApplicationPackageWithVersions(
495
+ "Drop versions first, or use --force to override."
496
+ )
497
+
498
+ # 4. Check distribution of the existing application package
499
+ actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
500
+ package_name=package_name,
501
+ package_role=package_role,
502
+ )
503
+ if not cls.verify_project_distribution(
504
+ console=console,
505
+ package_name=package_name,
506
+ package_role=package_role,
507
+ package_distribution=actual_distribution,
508
+ ):
509
+ console.warning(
510
+ f"Dropping application package {package_name} with distribution '{actual_distribution}'."
511
+ )
512
+
513
+ # 5. If distribution is internal, check if created by the Snowflake CLI
514
+ row_comment = show_obj_row[COMMENT_COL]
515
+ if actual_distribution == INTERNAL_DISTRIBUTION:
516
+ if row_comment in ALLOWED_SPECIAL_COMMENTS:
517
+ needs_confirm = False
518
+ else:
519
+ if needs_confirmation(needs_confirm, force_drop):
520
+ console.warning(
521
+ f"Application package {package_name} was not created by Snowflake CLI."
522
+ )
523
+ else:
524
+ if needs_confirmation(needs_confirm, force_drop):
525
+ console.warning(
526
+ 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."
527
+ )
528
+
529
+ if needs_confirmation(needs_confirm, force_drop):
530
+ should_drop_object = typer.confirm(
531
+ dedent(
532
+ f"""\
533
+ Application package details:
534
+ Name: {package_name}
535
+ Created on: {show_obj_row["created_on"]}
536
+ Distribution: {actual_distribution}
537
+ Owner: {show_obj_row[OWNER_COL]}
538
+ Comment: {show_obj_row[COMMENT_COL]}
539
+ Are you sure you want to drop it?
540
+ """
541
+ )
542
+ )
543
+ if not should_drop_object:
544
+ console.message(f"Did not drop application package {package_name}.")
545
+ return # The user desires to keep the application package, therefore exit gracefully
546
+
547
+ # All validations have passed, drop object
548
+ drop_generic_object(
549
+ console=console,
550
+ object_type="application package",
551
+ object_name=package_name,
552
+ role=package_role,
553
+ )
@@ -7,6 +7,8 @@ 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"
10
12
 
11
13
 
12
14
  T = TypeVar("T")
@@ -35,11 +37,13 @@ class EntityBase(Generic[T]):
35
37
  """
36
38
  return callable(getattr(self, action, None))
37
39
 
38
- def perform(self, action: EntityActions, action_ctx: ActionContext):
40
+ def perform(
41
+ self, action: EntityActions, action_ctx: ActionContext, *args, **kwargs
42
+ ):
39
43
  """
40
44
  Performs the requested action.
41
45
  """
42
- return getattr(self, action)(action_ctx)
46
+ return getattr(self, action)(action_ctx, *args, **kwargs)
43
47
 
44
48
 
45
49
  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,16 +25,18 @@ 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
@@ -272,8 +274,7 @@ def execute_post_deploy_hooks(
272
274
 
273
275
  scripts_content_list = render_script_templates(
274
276
  project_root,
275
- snowflake_sql_jinja_render,
276
- {},
277
+ get_cli_context().template_context,
277
278
  sql_scripts_paths,
278
279
  )
279
280
 
@@ -287,16 +288,17 @@ def execute_post_deploy_hooks(
287
288
 
288
289
  def render_script_templates(
289
290
  project_root: Path,
290
- render_from_str: Callable[[str, Dict[str, Any]], str],
291
291
  jinja_context: dict[str, Any],
292
292
  scripts: List[str],
293
+ override_env: Optional[jinja2.Environment] = None,
293
294
  ) -> List[str]:
294
295
  """
295
296
  Input:
296
297
  - project_root: path to project root
297
- - render_from_str: function which renders a jinja template from a string and jinja context
298
298
  - jinja_context: a dictionary with the jinja context
299
299
  - scripts: list of script paths relative to the project root
300
+ - override_env: optional jinja environment to use for rendering,
301
+ if not provided, the environment will be chosen based on the template syntax
300
302
  Returns:
301
303
  - List of rendered scripts content
302
304
  Size of the return list is the same as the size of the input scripts list.
@@ -306,16 +308,50 @@ def render_script_templates(
306
308
  script_full_path = SecurePath(project_root) / relpath
307
309
  try:
308
310
  template_content = script_full_path.read_text(file_size_limit_mb=UNLIMITED)
309
- result = render_from_str(template_content, jinja_context)
311
+ env = override_env or choose_sql_jinja_env_based_on_template_syntax(
312
+ template_content, reference_name=relpath
313
+ )
314
+ result = env.from_string(template_content).render(jinja_context)
310
315
  scripts_contents.append(result)
311
316
 
312
317
  except FileNotFoundError as e:
313
318
  raise MissingScriptError(relpath) from e
314
319
 
315
320
  except jinja2.TemplateSyntaxError as e:
316
- raise InvalidScriptError(relpath, e, e.lineno) from e
321
+ raise InvalidTemplateInFileError(relpath, e, e.lineno) from e
317
322
 
318
323
  except jinja2.UndefinedError as e:
319
- raise InvalidScriptError(relpath, e) from e
324
+ raise InvalidTemplateInFileError(relpath, e) from e
320
325
 
321
326
  return scripts_contents
327
+
328
+
329
+ def validation_item_to_str(item: dict[str, str | int]):
330
+ s = item["message"]
331
+ if item["errorCode"]:
332
+ s = f"{s} (error code {item['errorCode']})"
333
+ return s
334
+
335
+
336
+ def drop_generic_object(
337
+ console: AbstractConsole,
338
+ object_type: str,
339
+ object_name: str,
340
+ role: str,
341
+ cascade: bool = False,
342
+ ):
343
+ """
344
+ Drop object using the given role.
345
+ """
346
+ sql_executor = get_sql_executor()
347
+ with sql_executor.use_role(role):
348
+ console.step(f"Dropping {object_type} {object_name} now.")
349
+ drop_query = f"drop {object_type} {object_name}"
350
+ if cascade:
351
+ drop_query += " cascade"
352
+ try:
353
+ sql_executor.execute_query(drop_query)
354
+ except:
355
+ raise SnowflakeSQLExecutionError(drop_query)
356
+
357
+ console.message(f"Dropped {object_type} {object_name} successfully.")
@@ -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):
@@ -52,5 +52,3 @@ class FeatureFlag(FeatureFlagMixin):
52
52
  ENABLE_STREAMLIT_VERSIONED_STAGE = BooleanFlag(
53
53
  "ENABLE_STREAMLIT_VERSIONED_STAGE", False
54
54
  )
55
- # TODO: remove in 3.0
56
- ENABLE_PROJECT_DEFINITION_V2 = BooleanFlag("ENABLE_PROJECT_DEFINITION_V2", True)
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
  from typing import List, Optional
19
19
 
20
20
  import yaml
21
+ from click import ClickException
21
22
  from snowflake.cli.api.cli_global_context import get_cli_context
22
23
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
23
24
  from snowflake.cli.api.project.schemas.project_definition import (
@@ -45,8 +46,13 @@ def _get_merged_definitions(paths: List[Path]) -> Optional[Definition]:
45
46
  if len(spaths) == 0:
46
47
  return None
47
48
 
49
+ loader = yaml.BaseLoader
50
+ loader.add_constructor(
51
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_duplicates_constructor
52
+ )
53
+
48
54
  with spaths[0].open("r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as base_yml:
49
- definition = yaml.load(base_yml.read(), Loader=yaml.loader.BaseLoader) or {}
55
+ definition = yaml.load(base_yml.read(), Loader=loader) or {}
50
56
 
51
57
  for override_path in spaths[1:]:
52
58
  with override_path.open(
@@ -90,3 +96,20 @@ def default_role():
90
96
  def default_application(project_name: str):
91
97
  user = sanitize_identifier(get_env_username() or DEFAULT_USERNAME).lower()
92
98
  return append_to_identifier(to_identifier(project_name), f"_{user}")
99
+
100
+
101
+ def _no_duplicates_constructor(loader, node, deep=False):
102
+ """
103
+ Raises error it there are duplicated keys on the same level in the yaml file
104
+ """
105
+ mapping = {}
106
+
107
+ for key_node, value_node in node.value:
108
+ key = loader.construct_object(key_node, deep=deep)
109
+ value = loader.construct_object(value_node, deep=deep)
110
+ if key in mapping.keys():
111
+ raise ClickException(
112
+ f"While loading the project definition file, duplicate key was found: {key}"
113
+ )
114
+ mapping[key] = value
115
+ return loader.construct_mapping(node, deep)