snowflake-cli 3.2.2__py3-none-any.whl → 3.4.1__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 (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_app/constants.py +4 -0
  6. snowflake/cli/_app/snow_connector.py +12 -0
  7. snowflake/cli/_app/telemetry.py +10 -3
  8. snowflake/cli/_plugins/connection/util.py +12 -19
  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 +207 -1
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
  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 +42 -20
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
  19. snowflake/cli/_plugins/nativeapp/commands.py +113 -21
  20. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
  23. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  24. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  25. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  26. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
  27. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  28. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
  29. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  30. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  31. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
  32. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
  35. snowflake/cli/_plugins/notebook/commands.py +55 -2
  36. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  37. snowflake/cli/_plugins/notebook/manager.py +7 -5
  38. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  39. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  40. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  41. snowflake/cli/_plugins/notebook/types.py +3 -0
  42. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  43. snowflake/cli/_plugins/snowpark/common.py +47 -2
  44. snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
  45. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  46. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  47. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  48. snowflake/cli/_plugins/spcs/common.py +129 -0
  49. snowflake/cli/_plugins/spcs/services/commands.py +131 -14
  50. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  51. snowflake/cli/_plugins/stage/commands.py +2 -1
  52. snowflake/cli/_plugins/stage/diff.py +60 -39
  53. snowflake/cli/_plugins/stage/manager.py +34 -13
  54. snowflake/cli/_plugins/stage/utils.py +1 -1
  55. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  56. snowflake/cli/_plugins/streamlit/manager.py +70 -22
  57. snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
  58. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  59. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  60. snowflake/cli/_plugins/workspace/commands.py +6 -5
  61. snowflake/cli/_plugins/workspace/manager.py +9 -5
  62. snowflake/cli/api/artifacts/__init__.py +13 -0
  63. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  64. snowflake/cli/api/artifacts/common.py +78 -0
  65. snowflake/cli/api/artifacts/utils.py +82 -0
  66. snowflake/cli/api/cli_global_context.py +36 -2
  67. snowflake/cli/api/commands/flags.py +10 -4
  68. snowflake/cli/api/commands/utils.py +28 -2
  69. snowflake/cli/api/config.py +6 -2
  70. snowflake/cli/api/connections.py +12 -1
  71. snowflake/cli/api/constants.py +10 -1
  72. snowflake/cli/api/entities/common.py +81 -14
  73. snowflake/cli/api/entities/resolver.py +160 -0
  74. snowflake/cli/api/entities/utils.py +65 -23
  75. snowflake/cli/api/errno.py +63 -3
  76. snowflake/cli/api/feature_flags.py +19 -4
  77. snowflake/cli/api/metrics.py +21 -27
  78. snowflake/cli/api/project/definition_conversion.py +4 -4
  79. snowflake/cli/api/project/project_paths.py +28 -0
  80. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  81. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  82. snowflake/cli/api/project/schemas/project_definition.py +54 -6
  83. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  84. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  85. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  86. snowflake/cli/api/project/util.py +45 -0
  87. snowflake/cli/api/secure_path.py +6 -0
  88. snowflake/cli/api/sql_execution.py +5 -1
  89. snowflake/cli/api/stage_path.py +7 -2
  90. snowflake/cli/api/utils/graph.py +3 -0
  91. snowflake/cli/api/utils/path_utils.py +24 -0
  92. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
  93. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
  94. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
  95. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  96. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -14,17 +14,77 @@
14
14
 
15
15
  # General errors
16
16
  NO_WAREHOUSE_SELECTED_IN_SESSION = 606
17
+ EMPTY_SQL_STATEMENT = 900
17
18
 
18
- DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003
19
- DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = 2043
20
- INSUFFICIENT_PRIVILEGES = 3001
19
+ SQL_COMPILATION_ERROR = 1003
20
+ OBJECT_ALREADY_EXISTS_IN_DOMAIN = 1998
21
+ OBJECT_ALREADY_EXISTS = 2002
22
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 # BASE_TABLE_OR_VIEW_NOT_FOUND
23
+ DUPLICATE_COLUMN_NAME = 2025
24
+ VIEW_EXPANSION_FAILED = 2037
25
+ DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = (
26
+ 2043 # OBJECT_DOES_NOT_EXIST_OR_CANNOT_PERFORM_OPERATION
27
+ )
28
+ INSUFFICIENT_PRIVILEGES = 3001 # NOT_AUTHORIZED
29
+ INVALID_OBJECT_TYPE_FOR_SPECIFIED_PRIVILEGE = 3008
30
+ ROLE_NOT_ASSIGNED = 3013
31
+ NO_INDIVIDUAL_PRIVS = 3028
32
+ OBJECT_ALREADY_EXISTS_NO_PRIVILEGES = 3041
21
33
 
22
34
  # Native Apps
35
+ APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND = 93003
36
+ APPLICATION_FILE_NOT_FOUND_ON_STAGE = 93009
37
+ CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE = 93011
38
+ CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE = 93012
39
+ APPLICATION_PACKAGE_VERSION_ALREADY_EXISTS = 93030
40
+ APPLICATION_PACKAGE_VERSION_NAME_TOO_LONG = 93035
41
+ APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST = 93036
42
+ APPLICATION_PACKAGE_MAX_VERSIONS_HIT = 93037
23
43
  CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION = 93044
24
44
  CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES = 93045
25
45
  ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046
46
+ NO_VERSIONS_AVAILABLE_FOR_ACCOUNT = 93054
26
47
  NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055
27
48
  APPLICATION_NO_LONGER_AVAILABLE = 93079
49
+ APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082
50
+ APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE = 93083
51
+ APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT = 93084
52
+ VERSION_REFERENCED_BY_RELEASE_DIRECTIVE = 93088
53
+ APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148
54
+ CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118
28
55
  APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
56
+ APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168
57
+ CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER = 93167
58
+ APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197
59
+ NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301
60
+ NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302
61
+ NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR = 93303
62
+ NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX = 93300
29
63
  APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321
30
64
  CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
65
+ VERSION_NOT_ADDED_TO_RELEASE_CHANNEL = 512008
66
+ CANNOT_DISABLE_RELEASE_CHANNELS = 512001
67
+ RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND = 93036
68
+ RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH = 93074
69
+ RELEASE_DIRECTIVE_DOES_NOT_EXIST = 93090
70
+ VERSION_DOES_NOT_EXIST = 93031
71
+ CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH = 93170
72
+ VERSION_NOT_IN_RELEASE_CHANNEL = 512010
73
+ ACCOUNT_DOES_NOT_EXIST = 1999
74
+ ACCOUNT_HAS_TOO_MANY_QUALIFIERS = 906
75
+ CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS = 512017
76
+ VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL = 512005
77
+ MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED = 512004
78
+ MAX_UNBOUND_VERSIONS_REACHED = 512023
79
+ CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL = 512021
80
+ TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE = 93091
81
+
82
+
83
+ ERR_JAVASCRIPT_EXECUTION = 100132
84
+
85
+ SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL = 397007
86
+ SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND = 397012
87
+ SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW = 397013
88
+
89
+ NO_REFERENCE_SET_FOR_DEFINITION = 505019
90
+ NO_ACTIVE_REF_DEFINITION_WITH_REF_NAME_IN_APPLICATION = 505026
@@ -24,20 +24,31 @@ from snowflake.cli.api.config import (
24
24
 
25
25
  class BooleanFlag(NamedTuple):
26
26
  name: str
27
- default: bool = False
27
+ default: bool | None = False
28
28
 
29
29
 
30
30
  @unique
31
31
  class FeatureFlagMixin(Enum):
32
- def is_enabled(self) -> bool:
32
+ def get_value(self) -> bool | None:
33
33
  return get_config_bool_value(
34
34
  *FEATURE_FLAGS_SECTION_PATH,
35
35
  key=self.value.name.lower(),
36
36
  default=self.value.default,
37
37
  )
38
38
 
39
- def is_disabled(self):
40
- return not self.is_enabled()
39
+ def is_enabled(self) -> bool:
40
+ return self.get_value() is True
41
+
42
+ def is_disabled(self) -> bool:
43
+ return self.get_value() is False
44
+
45
+ def is_set(self) -> bool:
46
+ return (
47
+ get_config_bool_value(
48
+ *FEATURE_FLAGS_SECTION_PATH, key=self.value.name.lower(), default=None
49
+ )
50
+ is not None
51
+ )
41
52
 
42
53
  def env_variable(self):
43
54
  return get_env_variable_name(*FEATURE_FLAGS_SECTION_PATH, key=self.value.name)
@@ -52,3 +63,7 @@ class FeatureFlag(FeatureFlagMixin):
52
63
  ENABLE_STREAMLIT_VERSIONED_STAGE = BooleanFlag(
53
64
  "ENABLE_STREAMLIT_VERSIONED_STAGE", False
54
65
  )
66
+ ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
67
+ "ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
68
+ )
69
+ ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
@@ -116,10 +116,8 @@ class CLIMetricsSpan:
116
116
  children: Set[CLIMetricsSpan] = field(init=False, default_factory=set)
117
117
 
118
118
  # private state
119
- # start time of the step from the monotonic clock in order to calculate execution time
120
- _monotonic_start: float = field(
121
- init=False, default_factory=lambda: time.monotonic()
122
- )
119
+ # start time of the step from a performance counter in order to calculate execution time
120
+ _start_time: float = field(init=False, default_factory=time.perf_counter)
123
121
 
124
122
  def __hash__(self) -> int:
125
123
  return hash(self.span_id)
@@ -154,7 +152,7 @@ class CLIMetricsSpan:
154
152
  if error:
155
153
  self.error = error
156
154
 
157
- self.execution_time = time.monotonic() - self._monotonic_start
155
+ self.execution_time = time.perf_counter() - self._start_time
158
156
 
159
157
  def to_dict(self) -> Dict:
160
158
  """
@@ -191,9 +189,10 @@ class CLIMetrics:
191
189
  _in_progress_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
192
190
  # list of finished steps for telemetry to process
193
191
  _completed_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
194
- # monotonic clock time of when this class was initialized to approximate when the command first started executing
195
- _monotonic_start: float = field(
196
- init=False, default_factory=lambda: time.monotonic(), compare=False
192
+ # clock time of a performance counter when this class was initialized
193
+ # to approximate when the command first started executing
194
+ _start_time: float = field(
195
+ init=False, default_factory=time.perf_counter, compare=False
197
196
  )
198
197
 
199
198
  def clone(self) -> CLIMetrics:
@@ -223,7 +222,7 @@ class CLIMetrics:
223
222
  return self._in_progress_spans[-1] if len(self._in_progress_spans) > 0 else None
224
223
 
225
224
  @contextmanager
226
- def start_span(self, name: str) -> Iterator[CLIMetricsSpan]:
225
+ def span(self, name: str) -> Iterator[CLIMetricsSpan]:
227
226
  """
228
227
  Starts a new span that tracks various metrics throughout its execution
229
228
 
@@ -236,7 +235,7 @@ class CLIMetrics:
236
235
  """
237
236
  new_span = CLIMetricsSpan(
238
237
  name=name,
239
- start_time=time.monotonic() - self._monotonic_start,
238
+ start_time=time.perf_counter() - self._start_time,
240
239
  parent=self.current_span,
241
240
  )
242
241
 
@@ -275,34 +274,29 @@ class CLIMetrics:
275
274
  @property
276
275
  def completed_spans(self) -> List[Dict]:
277
276
  """
278
- Returns the completed spans tracked throughout a command, sorted by start time, for reporting telemetry
277
+ Returns the completed spans tracked throughout a command for reporting telemetry
279
278
 
280
279
  Ensures that the spans we send are within the configured limits and marks
281
280
  certain spans as trimmed if their children would bypass the limits we set
282
281
  """
283
282
  # take spans breadth-first within the depth and total limits
284
283
  # since we care more about the big picture than granularity
285
- spans_to_report = set(
286
- nsmallest(
287
- n=self.SPAN_TOTAL_LIMIT,
288
- iterable=(
289
- span
290
- for span in self._completed_spans
291
- if span.span_depth <= self.SPAN_DEPTH_LIMIT
292
- ),
293
- key=lambda span: (span.span_depth, span.start_time),
294
- )
284
+ spans_to_report = nsmallest(
285
+ n=self.SPAN_TOTAL_LIMIT,
286
+ iterable=(
287
+ span
288
+ for span in self._completed_spans
289
+ if span.span_depth <= self.SPAN_DEPTH_LIMIT
290
+ ),
291
+ key=lambda span: (span.span_depth, span.start_time, span.execution_time),
295
292
  )
296
293
 
297
- # sort by start time to make reading the payload easier
298
- sorted_spans_to_report = sorted(
299
- spans_to_report, key=lambda span: span.start_time
300
- )
294
+ spans_to_report_set = set(spans_to_report)
301
295
 
302
296
  return [
303
297
  {
304
298
  **span.to_dict(),
305
- CLIMetricsSpan.TRIMMED_KEY: not span.children <= spans_to_report,
299
+ CLIMetricsSpan.TRIMMED_KEY: not span.children <= spans_to_report_set,
306
300
  }
307
- for span in sorted_spans_to_report
301
+ for span in spans_to_report
308
302
  ]
@@ -13,7 +13,6 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
13
13
  bundle_artifacts,
14
14
  )
15
15
  from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
16
- from snowflake.cli._plugins.nativeapp.codegen.compiler import TEMPLATES_PROCESSOR
17
16
  from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import (
18
17
  TemplatesProcessor,
19
18
  )
@@ -223,10 +222,11 @@ def convert_streamlit_to_v2_data(streamlit: Streamlit) -> Dict[str, Any]:
223
222
  environment_file,
224
223
  pages_dir,
225
224
  ]
226
- artifacts = [a for a in artifacts if a is not None]
225
+ artifacts = [str(a) for a in artifacts if a is not None]
227
226
 
228
227
  if streamlit.additional_source_files:
229
- artifacts.extend(streamlit.additional_source_files)
228
+ for additional_file in streamlit.additional_source_files:
229
+ artifacts.append(str(additional_file))
230
230
 
231
231
  identifier = {"name": streamlit.name}
232
232
  if streamlit.schema_name:
@@ -457,7 +457,7 @@ def _convert_templates_in_files(
457
457
  artifact
458
458
  for artifact in pkg_model.artifacts
459
459
  for processor in artifact.processors
460
- if processor.name == TEMPLATES_PROCESSOR
460
+ if processor.name.lower() == TemplatesProcessor.NAME
461
461
  ]
462
462
  if not in_memory and artifacts_to_template:
463
463
  metrics.set_counter(CLICounterField.TEMPLATES_PROCESSOR, 1)
@@ -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,8 @@ 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
27
29
  from snowflake.cli._plugins.snowpark.snowpark_entity import (
28
30
  FunctionEntity,
29
31
  ProcedureEntity,
@@ -43,6 +45,7 @@ Entity = Union[
43
45
  StreamlitEntity,
44
46
  ProcedureEntity,
45
47
  FunctionEntity,
48
+ NotebookEntity,
46
49
  ]
47
50
  EntityModel = Union[
48
51
  ApplicationEntityModel,
@@ -50,6 +53,7 @@ EntityModel = Union[
50
53
  StreamlitEntityModel,
51
54
  FunctionEntityModel,
52
55
  ProcedureEntityModel,
56
+ NotebookEntityModel,
53
57
  ]
54
58
 
55
59
  ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
@@ -14,13 +14,19 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ from collections import defaultdict
17
18
  from dataclasses import dataclass
18
- from typing import Any, Dict, List, Optional, Union
19
+ from types import UnionType
20
+ from typing import Any, Dict, List, Optional, Union, get_args, get_origin
19
21
 
20
22
  from packaging.version import Version
21
23
  from pydantic import Field, ValidationError, field_validator, model_validator
22
24
  from pydantic_core.core_schema import ValidationInfo
23
25
  from snowflake.cli._plugins.nativeapp.entities.application import ApplicationEntityModel
26
+ from snowflake.cli._plugins.nativeapp.entities.application_package import (
27
+ ApplicationPackageChildrenTypes,
28
+ ApplicationPackageEntityModel,
29
+ )
24
30
  from snowflake.cli.api.project.errors import SchemaValidationError
25
31
  from snowflake.cli.api.project.schemas.entities.common import (
26
32
  TargetField,
@@ -159,6 +165,12 @@ class DefinitionV20(_ProjectDefinitionBase):
159
165
  target_object = entity.from_
160
166
  target_type = target_object.get_type()
161
167
  cls._validate_target_field(target_key, target_type, entities)
168
+ elif entity.type == ApplicationPackageEntityModel.get_type():
169
+ for child_entity in entity.children:
170
+ target_key = child_entity.target
171
+ cls._validate_target_field(
172
+ target_key, ApplicationPackageChildrenTypes, entities
173
+ )
162
174
 
163
175
  @classmethod
164
176
  def _validate_target_field(
@@ -168,11 +180,20 @@ class DefinitionV20(_ProjectDefinitionBase):
168
180
  raise ValueError(f"No such target: {target_key}")
169
181
 
170
182
  # Validate the target type
171
- actual_target_type = entities[target_key].__class__
172
- if target_type and target_type is not actual_target_type:
173
- raise ValueError(
174
- f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
175
- )
183
+ if target_type:
184
+ actual_target_type = entities[target_key].__class__
185
+ if get_origin(target_type) in (Union, UnionType):
186
+ if actual_target_type not in get_args(target_type):
187
+ expected_types_str = ", ".join(
188
+ [t.__name__ for t in get_args(target_type)]
189
+ )
190
+ raise ValueError(
191
+ f"Target type mismatch. Expected one of [{expected_types_str}], got {actual_target_type.__name__}"
192
+ )
193
+ elif target_type is not actual_target_type:
194
+ raise ValueError(
195
+ f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
196
+ )
176
197
 
177
198
  @model_validator(mode="before")
178
199
  @classmethod
@@ -200,6 +221,7 @@ class DefinitionV20(_ProjectDefinitionBase):
200
221
  mixin_defs=data["mixins"],
201
222
  )
202
223
  entities[entity_name] = merged_values
224
+
203
225
  return data
204
226
 
205
227
  @classmethod
@@ -241,6 +263,21 @@ class DefinitionV20(_ProjectDefinitionBase):
241
263
  data = cls._merge_data(data, entity)
242
264
  return data
243
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
+
244
281
  @classmethod
245
282
  def _merge_data(
246
283
  cls,
@@ -333,3 +370,14 @@ def _unique_extend(list_a: List, list_b: List) -> List:
333
370
  if all(item != x for x in list_a):
334
371
  new_list.append(item)
335
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
 
@@ -27,7 +27,7 @@ class Streamlit(UpdatableModel, ObjectIdentifierModel(object_name="Streamlit")):
27
27
  title="Stage in which the app’s artifacts will be stored", default="streamlit"
28
28
  )
29
29
  query_warehouse: str = Field(
30
- title="Snowflake warehouse to host the app", default="streamlit"
30
+ title="Snowflake warehouse to host the app", default=None
31
31
  )
32
32
  main_file: Optional[Path] = Field(
33
33
  title="Entrypoint file of the Streamlit app", default="streamlit_app.py"