snowflake-cli 3.3.0__py3-none-any.whl → 3.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +220 -197
  4. snowflake/cli/_app/commands_registration/builtin_plugins.py +5 -1
  5. snowflake/cli/_app/commands_registration/command_plugins_loader.py +3 -1
  6. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +4 -30
  7. snowflake/cli/_app/printing.py +2 -2
  8. snowflake/cli/_plugins/connection/commands.py +2 -4
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +3 -4
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
  19. snowflake/cli/_plugins/nativeapp/commands.py +21 -19
  20. snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
  21. snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
  22. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
  23. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
  24. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
  25. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
  26. snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
  27. snowflake/cli/_plugins/notebook/commands.py +54 -2
  28. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  29. snowflake/cli/_plugins/notebook/manager.py +3 -3
  30. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  31. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  32. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  33. snowflake/cli/_plugins/notebook/types.py +3 -0
  34. snowflake/cli/_plugins/plugin/commands.py +79 -0
  35. snowflake/cli/_plugins/plugin/manager.py +74 -0
  36. snowflake/cli/_plugins/plugin/plugin_spec.py +30 -0
  37. snowflake/cli/_plugins/project/__init__.py +0 -0
  38. snowflake/cli/_plugins/project/commands.py +157 -0
  39. snowflake/cli/_plugins/project/feature_flags.py +22 -0
  40. snowflake/cli/_plugins/project/manager.py +76 -0
  41. snowflake/cli/_plugins/project/plugin_spec.py +30 -0
  42. snowflake/cli/_plugins/project/project_entity_model.py +40 -0
  43. snowflake/cli/_plugins/snowpark/commands.py +49 -30
  44. snowflake/cli/_plugins/snowpark/common.py +47 -2
  45. snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
  46. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  47. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  48. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  49. snowflake/cli/_plugins/spcs/compute_pool/commands.py +53 -5
  50. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity.py +8 -0
  51. snowflake/cli/_plugins/spcs/compute_pool/compute_pool_entity_model.py +37 -0
  52. snowflake/cli/_plugins/spcs/compute_pool/manager.py +45 -0
  53. snowflake/cli/_plugins/spcs/image_repository/commands.py +29 -0
  54. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity.py +8 -0
  55. snowflake/cli/_plugins/spcs/image_repository/image_repository_entity_model.py +8 -0
  56. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  57. snowflake/cli/_plugins/spcs/services/commands.py +51 -1
  58. snowflake/cli/_plugins/spcs/services/manager.py +114 -0
  59. snowflake/cli/_plugins/spcs/services/service_entity.py +6 -0
  60. snowflake/cli/_plugins/spcs/services/service_entity_model.py +45 -0
  61. snowflake/cli/_plugins/spcs/services/service_project_paths.py +15 -0
  62. snowflake/cli/_plugins/stage/commands.py +2 -1
  63. snowflake/cli/_plugins/stage/diff.py +60 -39
  64. snowflake/cli/_plugins/stage/manager.py +26 -13
  65. snowflake/cli/_plugins/stage/utils.py +1 -1
  66. snowflake/cli/_plugins/streamlit/commands.py +18 -24
  67. snowflake/cli/_plugins/streamlit/manager.py +37 -27
  68. snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
  69. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  70. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  71. snowflake/cli/_plugins/workspace/commands.py +3 -3
  72. snowflake/cli/_plugins/workspace/manager.py +1 -1
  73. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  74. snowflake/cli/api/artifacts/common.py +78 -0
  75. snowflake/cli/api/artifacts/upload.py +51 -0
  76. snowflake/cli/api/artifacts/utils.py +82 -0
  77. snowflake/cli/api/cli_global_context.py +14 -1
  78. snowflake/cli/api/commands/flags.py +34 -13
  79. snowflake/cli/api/commands/snow_typer.py +12 -0
  80. snowflake/cli/api/commands/utils.py +30 -2
  81. snowflake/cli/api/config.py +15 -10
  82. snowflake/cli/api/constants.py +1 -0
  83. snowflake/cli/api/entities/common.py +14 -32
  84. snowflake/cli/api/entities/resolver.py +160 -0
  85. snowflake/cli/api/entities/utils.py +56 -15
  86. snowflake/cli/api/errno.py +3 -0
  87. snowflake/cli/api/exceptions.py +8 -1
  88. snowflake/cli/api/feature_flags.py +1 -1
  89. snowflake/cli/api/plugins/plugin_config.py +43 -4
  90. snowflake/cli/api/project/definition_conversion.py +3 -2
  91. snowflake/cli/api/project/definition_helper.py +31 -0
  92. snowflake/cli/api/project/project_paths.py +28 -0
  93. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  94. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  95. snowflake/cli/api/project/schemas/project_definition.py +27 -0
  96. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  97. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  98. snowflake/cli/api/secure_path.py +6 -0
  99. snowflake/cli/api/sql_execution.py +5 -1
  100. snowflake/cli/api/stage_path.py +7 -2
  101. snowflake/cli/api/utils/graph.py +3 -0
  102. snowflake/cli/api/utils/path_utils.py +24 -0
  103. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/METADATA +12 -13
  104. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/RECORD +109 -85
  105. snowflake/cli/_app/api_impl/plugin/plugin_config_provider_impl.py +0 -66
  106. snowflake/cli/api/__init__.py +0 -48
  107. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  108. /snowflake/cli/{_app/api_impl → _plugins/plugin}/__init__.py +0 -0
  109. /snowflake/cli/{_app/api_impl/plugin → api/artifacts}/__init__.py +0 -0
  110. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/WHEEL +0 -0
  111. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/entry_points.txt +0 -0
  112. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,10 @@
1
1
  import os
2
+ from enum import Enum
2
3
  from pathlib import Path
3
4
  from typing import Any, List, NoReturn, Optional
4
5
 
5
6
  import jinja2
6
7
  from click import ClickException
7
- from snowflake.cli._plugins.nativeapp.artifacts import (
8
- BundleMap,
9
- resolve_without_follow,
10
- )
11
8
  from snowflake.cli._plugins.nativeapp.exceptions import (
12
9
  InvalidTemplateInFileError,
13
10
  MissingScriptError,
@@ -22,10 +19,11 @@ from snowflake.cli._plugins.stage.diff import (
22
19
  sync_local_diff_with_stage,
23
20
  to_stage_path,
24
21
  )
22
+ from snowflake.cli._plugins.stage.manager import DefaultStagePathParts
25
23
  from snowflake.cli._plugins.stage.utils import print_diff_to_console
24
+ from snowflake.cli.api.artifacts.bundle_map import BundleMap
26
25
  from snowflake.cli.api.cli_global_context import get_cli_context, span
27
26
  from snowflake.cli.api.console.abc import AbstractConsole
28
- from snowflake.cli.api.entities.common import get_sql_executor
29
27
  from snowflake.cli.api.errno import (
30
28
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
31
29
  NO_WAREHOUSE_SELECTED_IN_SESSION,
@@ -41,6 +39,8 @@ from snowflake.cli.api.rendering.sql_templates import (
41
39
  choose_sql_jinja_env_based_on_template_syntax,
42
40
  )
43
41
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
42
+ from snowflake.cli.api.sql_execution import SqlExecutor
43
+ from snowflake.cli.api.utils.path_utils import resolve_without_follow
44
44
  from snowflake.connector import ProgrammingError
45
45
 
46
46
 
@@ -80,12 +80,11 @@ def sync_deploy_root_with_stage(
80
80
  console: AbstractConsole,
81
81
  deploy_root: Path,
82
82
  package_name: str,
83
- stage_schema: str,
84
83
  bundle_map: BundleMap,
85
84
  role: str,
86
85
  prune: bool,
87
86
  recursive: bool,
88
- stage_fqn: str,
87
+ stage_path: DefaultStagePathParts,
89
88
  local_paths_to_sync: List[Path] | None = None,
90
89
  print_diff: bool = True,
91
90
  ) -> DiffResult:
@@ -98,32 +97,37 @@ def sync_deploy_root_with_stage(
98
97
  role (str): The name of the role to use for queries and commands.
99
98
  prune (bool): Whether to prune artifacts from the stage that don't exist locally.
100
99
  recursive (bool): Whether to traverse directories recursively.
101
- stage_fqn (str): The name of the stage to diff against and upload to.
100
+ stage_path (DefaultStagePathParts): stage path object.
101
+
102
102
  local_paths_to_sync (List[Path], optional): List of local paths to sync. Defaults to None to sync all
103
- local paths. Note that providing an empty list here is equivalent to None.
103
+ local paths. Note that providing an empty list here is equivalent to None.
104
104
  print_diff (bool): Whether to print the diff between the local files and the remote stage. Defaults to True
105
105
 
106
106
  Returns:
107
107
  A `DiffResult` instance describing the changes that were performed.
108
108
  """
109
-
110
109
  sql_facade = get_snowflake_facade()
110
+ schema = stage_path.schema
111
+ stage_fqn = stage_path.stage
111
112
  # Does a stage already exist within the application package, or we need to create one?
112
113
  # Using "if not exists" should take care of either case.
113
114
  console.step(
114
115
  f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
115
116
  )
116
117
  if not sql_facade.stage_exists(stage_fqn):
117
- sql_facade.create_schema(stage_schema, database=package_name)
118
+ sql_facade.create_schema(schema, database=package_name)
118
119
  sql_facade.create_stage(stage_fqn)
119
120
 
120
121
  # Perform a diff operation and display results to the user for informational purposes
121
122
  if print_diff:
122
123
  console.step(
123
- "Performing a diff between the Snowflake stage and your local deploy_root ('%s') directory."
124
- % deploy_root.resolve()
124
+ f"Performing a diff between the Snowflake stage: {stage_path.path} and your local deploy_root: {deploy_root.resolve()}."
125
125
  )
126
- diff: DiffResult = compute_stage_diff(deploy_root, stage_fqn)
126
+
127
+ diff: DiffResult = compute_stage_diff(
128
+ local_root=deploy_root,
129
+ stage_path=stage_path,
130
+ )
127
131
 
128
132
  if local_paths_to_sync:
129
133
  # Deploying specific files/directories
@@ -184,7 +188,7 @@ def sync_deploy_root_with_stage(
184
188
  role=role,
185
189
  deploy_root_path=deploy_root,
186
190
  diff_result=diff,
187
- stage_fqn=stage_fqn,
191
+ stage_full_path=stage_path.full_path,
188
192
  )
189
193
  return diff
190
194
 
@@ -336,3 +340,40 @@ def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]):
336
340
  for message in messages:
337
341
  console.warning(message)
338
342
  console.message("")
343
+
344
+
345
+ def get_sql_executor() -> SqlExecutor:
346
+ """Returns an SQL Executor that uses the connection from the current CLI context"""
347
+ return SqlExecutor()
348
+
349
+
350
+ class EntityActions(str, Enum):
351
+ BUNDLE = "action_bundle"
352
+ DEPLOY = "action_deploy"
353
+ DROP = "action_drop"
354
+ VALIDATE = "action_validate"
355
+ EVENTS = "action_events"
356
+ DIFF = "action_diff"
357
+
358
+ VERSION_LIST = "action_version_list"
359
+ VERSION_CREATE = "action_version_create"
360
+ VERSION_DROP = "action_version_drop"
361
+
362
+ RELEASE_DIRECTIVE_UNSET = "action_release_directive_unset"
363
+ RELEASE_DIRECTIVE_SET = "action_release_directive_set"
364
+ RELEASE_DIRECTIVE_LIST = "action_release_directive_list"
365
+ RELEASE_DIRECTIVE_ADD_ACCOUNTS = "action_release_directive_add_accounts"
366
+ RELEASE_DIRECTIVE_REMOVE_ACCOUNTS = "action_release_directive_remove_accounts"
367
+
368
+ RELEASE_CHANNEL_LIST = "action_release_channel_list"
369
+ RELEASE_CHANNEL_ADD_ACCOUNTS = "action_release_channel_add_accounts"
370
+ RELEASE_CHANNEL_REMOVE_ACCOUNTS = "action_release_channel_remove_accounts"
371
+ RELEASE_CHANNEL_ADD_VERSION = "action_release_channel_add_version"
372
+ RELEASE_CHANNEL_REMOVE_VERSION = "action_release_channel_remove_version"
373
+ RELEASE_CHANNEL_SET_ACCOUNTS = "action_release_channel_set_accounts"
374
+
375
+ PUBLISH = "action_publish"
376
+
377
+ @property
378
+ def get_action_name(self):
379
+ return self.value.replace("action_", "")
@@ -54,6 +54,7 @@ APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148
54
54
  CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118
55
55
  APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
56
56
  APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168
57
+ CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER = 93167
57
58
  APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197
58
59
  NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301
59
60
  NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302
@@ -64,8 +65,10 @@ CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
64
65
  VERSION_NOT_ADDED_TO_RELEASE_CHANNEL = 512008
65
66
  CANNOT_DISABLE_RELEASE_CHANNELS = 512001
66
67
  RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND = 93036
68
+ RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH = 93074
67
69
  RELEASE_DIRECTIVE_DOES_NOT_EXIST = 93090
68
70
  VERSION_DOES_NOT_EXIST = 93031
71
+ CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH = 93170
69
72
  VERSION_NOT_IN_RELEASE_CHANNEL = 512010
70
73
  ACCOUNT_DOES_NOT_EXIST = 1999
71
74
  ACCOUNT_HAS_TOO_MANY_QUALIFIERS = 906
@@ -15,7 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from pathlib import Path
18
- from typing import Optional
18
+ from typing import List, Optional
19
19
 
20
20
  from click.exceptions import ClickException, UsageError
21
21
  from snowflake.cli.api.constants import ObjectType
@@ -54,6 +54,13 @@ class InvalidPluginConfiguration(ClickException):
54
54
  return f"Invalid plugin configuration. {self.message}"
55
55
 
56
56
 
57
+ class PluginNotInstalledError(ClickException):
58
+ def __init__(self, plugin_name, installed_plugins: List[str]):
59
+ super().__init__(
60
+ f"Plugin {plugin_name} is not installed. Available plugins: {', '.join(installed_plugins)}."
61
+ )
62
+
63
+
57
64
  class SnowflakeConnectionError(ClickException):
58
65
  def __init__(self, snowflake_err: Exception):
59
66
  super().__init__(f"Could not connect to Snowflake. Reason: {snowflake_err}")
@@ -66,5 +66,5 @@ class FeatureFlag(FeatureFlagMixin):
66
66
  ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
67
67
  "ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
68
68
  )
69
+ ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
69
70
  ENABLE_SPCS_SERVICE_EVENTS = BooleanFlag("ENABLE_SPCS_SERVICE_EVENTS", False)
70
- ENABLE_SPCS_SERVICE_METRICS = BooleanFlag("ENABLE_SPCS_SERVICE_METRICS", False)
@@ -17,6 +17,16 @@ from __future__ import annotations
17
17
  from dataclasses import dataclass
18
18
  from typing import Any, Dict, List
19
19
 
20
+ from snowflake.cli.api.config import (
21
+ PLUGIN_ENABLED_KEY,
22
+ PLUGINS_SECTION_PATH,
23
+ config_section_exists,
24
+ get_config_section,
25
+ get_config_value,
26
+ get_plugins_config,
27
+ )
28
+ from snowflake.cli.api.exceptions import InvalidPluginConfiguration
29
+
20
30
 
21
31
  @dataclass
22
32
  class PluginConfig:
@@ -25,8 +35,37 @@ class PluginConfig:
25
35
 
26
36
 
27
37
  class PluginConfigProvider:
28
- def get_enabled_plugin_names(self) -> List[str]:
29
- raise NotImplementedError()
38
+ @staticmethod
39
+ def get_enabled_plugin_names() -> List[str]:
40
+ enabled_plugins = []
41
+ for plugin_name, plugin_config_section in get_plugins_config().items():
42
+ enabled = plugin_config_section.get(PLUGIN_ENABLED_KEY, False)
43
+ _assert_value_is_bool(
44
+ enabled, value_name=PLUGIN_ENABLED_KEY, plugin_name=plugin_name
45
+ )
46
+ if enabled:
47
+ enabled_plugins.append(plugin_name)
48
+ return enabled_plugins
49
+
50
+ @staticmethod
51
+ def get_config(plugin_name: str) -> PluginConfig:
52
+ config_path = PLUGINS_SECTION_PATH + [plugin_name]
53
+ plugin_config = PluginConfig(is_plugin_enabled=False, internal_config={})
54
+ plugin_config.is_plugin_enabled = get_config_value(
55
+ *config_path, key=PLUGIN_ENABLED_KEY, default=False
56
+ )
57
+ _assert_value_is_bool(
58
+ plugin_config.is_plugin_enabled,
59
+ value_name=PLUGIN_ENABLED_KEY,
60
+ plugin_name=plugin_name,
61
+ )
62
+ if config_section_exists(*config_path, "config"):
63
+ plugin_config.internal_config = get_config_section(*config_path, "config")
64
+ return plugin_config
65
+
30
66
 
31
- def get_config(self, plugin_name: str) -> PluginConfig:
32
- raise NotImplementedError()
67
+ def _assert_value_is_bool(value, *, value_name: str, plugin_name: str) -> None:
68
+ if type(value) is not bool:
69
+ raise InvalidPluginConfiguration(
70
+ f'[{plugin_name}]: "{value_name}" must be a boolean'
71
+ )
@@ -222,10 +222,11 @@ def convert_streamlit_to_v2_data(streamlit: Streamlit) -> Dict[str, Any]:
222
222
  environment_file,
223
223
  pages_dir,
224
224
  ]
225
- artifacts = [a for a in artifacts if a is not None]
225
+ artifacts = [str(a) for a in artifacts if a is not None]
226
226
 
227
227
  if streamlit.additional_source_files:
228
- artifacts.extend(streamlit.additional_source_files)
228
+ for additional_file in streamlit.additional_source_files:
229
+ artifacts.append(str(additional_file))
229
230
 
230
231
  identifier = {"name": streamlit.name}
231
232
  if streamlit.schema_name:
@@ -0,0 +1,31 @@
1
+ from typing import Optional
2
+
3
+ from click import UsageError
4
+ from snowflake.cli.api.cli_global_context import get_cli_context
5
+ from snowflake.cli.api.constants import ObjectType
6
+ from snowflake.cli.api.exceptions import NoProjectDefinitionError
7
+
8
+
9
+ def get_entity_from_project_definition(
10
+ entity_type: ObjectType, entity_id: Optional[str] = None
11
+ ):
12
+ cli_context = get_cli_context()
13
+ pd = cli_context.project_definition
14
+ entities = pd.get_entities_by_type(entity_type=entity_type.value.cli_name)
15
+
16
+ if not entities:
17
+ raise NoProjectDefinitionError(
18
+ project_type=entity_type.value.sf_name,
19
+ project_root=cli_context.project_root,
20
+ )
21
+
22
+ if entity_id and entity_id not in entities:
23
+ raise UsageError(f"No '{entity_id}' entity in project definition file.")
24
+ elif len(entities.keys()) == 1:
25
+ entity_id = list(entities.keys())[0]
26
+
27
+ if entity_id is None:
28
+ raise UsageError(
29
+ f"Multiple {entity_type.value.sf_plural_name} found. Please provide entity id for the operation."
30
+ )
31
+ return entities[entity_id]
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ from snowflake.cli.api.secure_path import SecurePath
5
+
6
+
7
+ @dataclass
8
+ class ProjectPaths:
9
+ """
10
+ This class allows you to manage files paths related to given project.
11
+ Class provides bundle root path and allows to remove it.
12
+ """
13
+
14
+ project_root: Path
15
+
16
+ @property
17
+ def bundle_root(self) -> Path:
18
+ return bundle_root(self.project_root)
19
+
20
+ def remove_up_bundle_root(self) -> None:
21
+ if self.bundle_root.exists():
22
+ SecurePath(self.bundle_root).rmdir(recursive=True)
23
+
24
+
25
+ def bundle_root(root: Path, app_type: str | None = None) -> Path:
26
+ if app_type:
27
+ return root / "output" / "bundle" / app_type
28
+ return root / "output" / "bundle"
@@ -15,9 +15,10 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from abc import ABC
18
- from typing import Dict, Generic, List, Optional, TypeVar, Union
18
+ from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
19
19
 
20
20
  from pydantic import Field, PrivateAttr, field_validator
21
+ from pydantic_core.core_schema import ValidationInfo
21
22
  from snowflake.cli.api.identifiers import FQN
22
23
  from snowflake.cli.api.project.schemas.updatable_model import (
23
24
  IdentifierField,
@@ -61,6 +62,15 @@ class MetaField(UpdatableModel):
61
62
  default=None,
62
63
  )
63
64
 
65
+ depends_on: Optional[List[str]] = Field(
66
+ title="Entities that need to be deployed before this one", default_factory=list
67
+ )
68
+
69
+ action_arguments: Optional[Dict[str, Dict[str, Union[int, bool, str]]]] = Field(
70
+ title="Arguments that will be used, when this entity is called as a dependency of other entity",
71
+ default_factory=dict,
72
+ )
73
+
64
74
  @field_validator("use_mixins", mode="before")
65
75
  @classmethod
66
76
  def ensure_use_mixins_is_a_list(
@@ -70,6 +80,35 @@ class MetaField(UpdatableModel):
70
80
  return [mixins]
71
81
  return mixins
72
82
 
83
+ @field_validator("action_arguments", mode="before")
84
+ @classmethod
85
+ def arguments_validator(cls, arguments: Dict, info: ValidationInfo) -> Dict:
86
+ duplicated_run = (
87
+ info.context.get("is_duplicated_run", False) if info.context else False
88
+ )
89
+ if not duplicated_run:
90
+ for argument_dict in arguments.values():
91
+ for k, v in argument_dict.items():
92
+ argument_dict[k] = cls._cast_value(v)
93
+
94
+ return arguments
95
+
96
+ @staticmethod
97
+ def _cast_value(value: str) -> Union[int, bool, str]:
98
+ if value.lower() in ["true", "false"]:
99
+ return value.lower() == "true"
100
+
101
+ try:
102
+ return int(value)
103
+ except ValueError:
104
+ return value
105
+
106
+ def __eq__(self, other):
107
+ return self.entity_id == other.entity_id
108
+
109
+ def __hash__(self):
110
+ return hash(self.entity_id)
111
+
73
112
 
74
113
  class Identifier(UpdatableModel):
75
114
  name: str = Field(title="Entity name")
@@ -141,6 +180,23 @@ class ImportsBaseModel:
141
180
  return f"IMPORTS = ({imports})"
142
181
 
143
182
 
183
+ class Grant(UpdatableModel):
184
+ privilege: str = Field(title="Required privileges")
185
+ role: str = Field(title="Role to which the privileges will be granted")
186
+
187
+ def get_grant_sql(self, entity_model: EntityModelBase) -> str:
188
+ return f"GRANT {self.privilege} ON {entity_model.get_type().upper()} {entity_model.fqn.sql_identifier} TO ROLE {self.role}"
189
+
190
+
191
+ class GrantBaseModel(UpdatableModel):
192
+ grants: Optional[List[Grant]] = Field(title="List of grants", default=None)
193
+
194
+ def get_grant_sqls(self) -> list[str]:
195
+ return (
196
+ [grant.get_grant_sql(self) for grant in self.grants] if self.grants else []
197
+ )
198
+
199
+
144
200
  class ExternalAccessBaseModel:
145
201
  external_access_integrations: Optional[List[str]] = Field(
146
202
  title="Names of external access integrations needed for this entity to access external networks",
@@ -162,3 +218,76 @@ class ExternalAccessBaseModel:
162
218
  return None
163
219
  secrets = ", ".join(f"'{key}'={value}" for key, value in self.secrets.items())
164
220
  return f"secrets=({secrets})"
221
+
222
+
223
+ class ProcessorMapping(UpdatableModel):
224
+ name: str = Field(
225
+ title="Name of a processor to invoke on a collection of artifacts."
226
+ )
227
+ properties: Optional[Dict[str, Any]] = Field(
228
+ title="A set of key-value pairs used to configure the output of the processor. Consult a specific processor's documentation for more details on the supported properties.",
229
+ default=None,
230
+ )
231
+
232
+
233
+ class PathMapping(UpdatableModel):
234
+ src: str = Field(
235
+ title="Source path or glob pattern (relative to project root)", default=None
236
+ )
237
+
238
+ dest: Optional[str] = Field(
239
+ title="Destination path on stage",
240
+ description="Paths are relative to stage root; paths ending with a slash indicate that the destination is a directory which source files should be copied into.",
241
+ default=None,
242
+ )
243
+
244
+ processors: Optional[List[Union[str, ProcessorMapping]]] = Field(
245
+ title="List of processors to apply to matching source files during bundling.",
246
+ default=[],
247
+ )
248
+
249
+ @field_validator("processors")
250
+ @classmethod
251
+ def transform_processors(
252
+ cls, input_values: Optional[List[Union[str, Dict, ProcessorMapping]]]
253
+ ) -> List[ProcessorMapping]:
254
+ if input_values is None:
255
+ return []
256
+
257
+ transformed_processors: List[ProcessorMapping] = []
258
+ for input_processor in input_values:
259
+ if isinstance(input_processor, str):
260
+ transformed_processors.append(ProcessorMapping(name=input_processor))
261
+ elif isinstance(input_processor, Dict):
262
+ transformed_processors.append(ProcessorMapping(**input_processor))
263
+ else:
264
+ transformed_processors.append(input_processor)
265
+ return transformed_processors
266
+
267
+
268
+ Artifacts = List[Union[PathMapping, str]]
269
+
270
+
271
+ class EntityModelBaseWithArtifacts(EntityModelBase):
272
+ artifacts: Artifacts = Field(
273
+ title="List of paths or file source/destination pairs to add to the deploy root",
274
+ )
275
+ deploy_root: Optional[str] = Field(
276
+ title="Folder at the root of your project where the build step copies the artifacts",
277
+ default="output/deploy/",
278
+ )
279
+
280
+ @field_validator("artifacts")
281
+ @classmethod
282
+ def transform_artifacts(cls, orig_artifacts: Artifacts) -> List[PathMapping]:
283
+ transformed_artifacts: List[PathMapping] = []
284
+ if orig_artifacts is None:
285
+ return transformed_artifacts
286
+
287
+ for artifact in orig_artifacts:
288
+ if isinstance(artifact, PathMapping):
289
+ transformed_artifacts.append(artifact)
290
+ else:
291
+ transformed_artifacts.append(PathMapping(src=artifact))
292
+
293
+ return transformed_artifacts
@@ -24,6 +24,12 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import (
24
24
  ApplicationPackageEntity,
25
25
  ApplicationPackageEntityModel,
26
26
  )
27
+ from snowflake.cli._plugins.notebook.notebook_entity import NotebookEntity
28
+ from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntityModel
29
+ from snowflake.cli._plugins.project.project_entity_model import (
30
+ ProjectEntity,
31
+ ProjectEntityModel,
32
+ )
27
33
  from snowflake.cli._plugins.snowpark.snowpark_entity import (
28
34
  FunctionEntity,
29
35
  ProcedureEntity,
@@ -32,6 +38,20 @@ from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
32
38
  FunctionEntityModel,
33
39
  ProcedureEntityModel,
34
40
  )
41
+ from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity import (
42
+ ComputePoolEntity,
43
+ )
44
+ from snowflake.cli._plugins.spcs.compute_pool.compute_pool_entity_model import (
45
+ ComputePoolEntityModel,
46
+ )
47
+ from snowflake.cli._plugins.spcs.image_repository.image_repository_entity import (
48
+ ImageRepositoryEntity,
49
+ )
50
+ from snowflake.cli._plugins.spcs.image_repository.image_repository_entity_model import (
51
+ ImageRepositoryEntityModel,
52
+ )
53
+ from snowflake.cli._plugins.spcs.services.service_entity import ServiceEntity
54
+ from snowflake.cli._plugins.spcs.services.service_entity_model import ServiceEntityModel
35
55
  from snowflake.cli._plugins.streamlit.streamlit_entity import StreamlitEntity
36
56
  from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
37
57
  StreamlitEntityModel,
@@ -42,7 +62,12 @@ Entity = Union[
42
62
  ApplicationPackageEntity,
43
63
  StreamlitEntity,
44
64
  ProcedureEntity,
65
+ ProjectEntity,
45
66
  FunctionEntity,
67
+ ComputePoolEntity,
68
+ ImageRepositoryEntity,
69
+ ServiceEntity,
70
+ NotebookEntity,
46
71
  ]
47
72
  EntityModel = Union[
48
73
  ApplicationEntityModel,
@@ -50,6 +75,11 @@ EntityModel = Union[
50
75
  StreamlitEntityModel,
51
76
  FunctionEntityModel,
52
77
  ProcedureEntityModel,
78
+ ComputePoolEntityModel,
79
+ ImageRepositoryEntityModel,
80
+ ServiceEntityModel,
81
+ NotebookEntityModel,
82
+ ProjectEntityModel,
53
83
  ]
54
84
 
55
85
  ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
@@ -14,6 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ from collections import defaultdict
17
18
  from dataclasses import dataclass
18
19
  from types import UnionType
19
20
  from typing import Any, Dict, List, Optional, Union, get_args, get_origin
@@ -262,6 +263,21 @@ class DefinitionV20(_ProjectDefinitionBase):
262
263
  data = cls._merge_data(data, entity)
263
264
  return data
264
265
 
266
+ @model_validator(mode="after")
267
+ def validate_dependencies(self):
268
+ """
269
+ Checks if entities listed in depends_on section exist in the project
270
+ """
271
+ missing_dependencies = defaultdict(list)
272
+ for entity_id, entity in self.entities.items():
273
+ if entity.meta:
274
+ for dependency in entity.meta.depends_on:
275
+ if dependency not in self.entities:
276
+ missing_dependencies[entity_id].append(dependency)
277
+
278
+ if missing_dependencies:
279
+ raise ValueError(_get_missing_dependencies_message(missing_dependencies))
280
+
265
281
  @classmethod
266
282
  def _merge_data(
267
283
  cls,
@@ -354,3 +370,14 @@ def _unique_extend(list_a: List, list_b: List) -> List:
354
370
  if all(item != x for x in list_a):
355
371
  new_list.append(item)
356
372
  return new_list
373
+
374
+
375
+ def _get_missing_dependencies_message(
376
+ missing_dependencies: Dict[str, List[str]]
377
+ ) -> str:
378
+ missing_dependencies_message = []
379
+ for entity_id, dependencies in missing_dependencies.items():
380
+ missing_dependencies_message.append(
381
+ f"\n Entity {entity_id} depends on non-existing entities: {', '.join(dependencies)}"
382
+ )
383
+ return "".join(missing_dependencies_message)
@@ -122,10 +122,10 @@ class UpdatableModel(BaseModel):
122
122
  class_dict = class_.__dict__
123
123
  field_annotations.update(class_dict.get("__annotations__", {}))
124
124
 
125
- if "model_fields" in class_dict:
125
+ if "model_fields" in class_dict and class_.model_fields:
126
126
  # This means the class dict has already been processed by Pydantic
127
127
  # All fields should properly be populated in model_fields
128
- field_values.update(class_dict["model_fields"])
128
+ field_values.update(class_.model_fields)
129
129
  else:
130
130
  # If Pydantic did not process this class yet, get the values from class_dict directly
131
131
  field_values.update(class_dict)
@@ -15,16 +15,16 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import re
18
- from typing import List, Optional, Union
18
+ from typing import List, Optional
19
19
 
20
20
  from pydantic import Field, field_validator
21
+ from snowflake.cli.api.project.schemas.entities.common import Artifacts, PathMapping
21
22
  from snowflake.cli.api.project.schemas.updatable_model import UpdatableModel
22
23
  from snowflake.cli.api.project.schemas.v1.native_app.application import (
23
24
  Application,
24
25
  ApplicationV11,
25
26
  )
26
27
  from snowflake.cli.api.project.schemas.v1.native_app.package import Package, PackageV11
27
- from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
28
28
  from snowflake.cli.api.project.util import (
29
29
  SCHEMA_AND_NAME,
30
30
  )
@@ -34,7 +34,7 @@ class NativeApp(UpdatableModel):
34
34
  name: str = Field(
35
35
  title="Project identifier",
36
36
  )
37
- artifacts: List[Union[PathMapping, str]] = Field(
37
+ artifacts: Artifacts = Field(
38
38
  title="List of file source and destination pairs to add to the deploy root",
39
39
  )
40
40
  bundle_root: Optional[str] = Field(
@@ -69,10 +69,8 @@ class NativeApp(UpdatableModel):
69
69
 
70
70
  @field_validator("artifacts")
71
71
  @classmethod
72
- def transform_artifacts(
73
- cls, orig_artifacts: List[Union[PathMapping, str]]
74
- ) -> List[PathMapping]:
75
- transformed_artifacts = []
72
+ def transform_artifacts(cls, orig_artifacts: Artifacts) -> List[PathMapping]:
73
+ transformed_artifacts: List[PathMapping] = []
76
74
  if orig_artifacts is None:
77
75
  return transformed_artifacts
78
76
 
@@ -104,6 +104,12 @@ class SecurePath:
104
104
  """
105
105
  return self._path.is_file()
106
106
 
107
+ def glob(self, pattern: str):
108
+ """
109
+ Return a generator yielding Path objects that match the given pattern.
110
+ """
111
+ return self._path.glob(pattern)
112
+
107
113
  @property
108
114
  def name(self) -> str:
109
115
  """A string representing the final path component."""
@@ -92,7 +92,11 @@ class BaseSqlExecutor:
92
92
 
93
93
  def execute_queries(self, queries: str, **kwargs):
94
94
  """Executes multiple SQL queries (passed as one string) and returns the results as a list"""
95
- return list(self._execute_string(dedent(queries), **kwargs))
95
+
96
+ # Without remove_comments=True, connectors might throw an error if there is a comment at the end of the file
97
+ return list(
98
+ self._execute_string(dedent(queries), remove_comments=True, **kwargs)
99
+ )
96
100
 
97
101
 
98
102
  class SqlExecutor(BaseSqlExecutor):