snowflake-cli-labs 3.0.0rc1__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 (45) 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 +76 -29
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/git/commands.py +55 -14
  7. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -5
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +49 -31
  9. snowflake/cli/_plugins/nativeapp/manager.py +46 -87
  10. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  11. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  12. snowflake/cli/_plugins/nativeapp/teardown_processor.py +9 -152
  13. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  14. snowflake/cli/_plugins/snowpark/commands.py +1 -1
  15. snowflake/cli/_plugins/snowpark/models.py +2 -1
  16. snowflake/cli/_plugins/streamlit/commands.py +1 -1
  17. snowflake/cli/_plugins/streamlit/manager.py +9 -0
  18. snowflake/cli/_plugins/workspace/action_context.py +2 -1
  19. snowflake/cli/_plugins/workspace/commands.py +48 -16
  20. snowflake/cli/_plugins/workspace/manager.py +1 -0
  21. snowflake/cli/api/cli_global_context.py +136 -313
  22. snowflake/cli/api/commands/flags.py +76 -91
  23. snowflake/cli/api/commands/snow_typer.py +6 -4
  24. snowflake/cli/api/config.py +1 -1
  25. snowflake/cli/api/connections.py +214 -0
  26. snowflake/cli/api/console/abc.py +4 -2
  27. snowflake/cli/api/entities/application_entity.py +687 -2
  28. snowflake/cli/api/entities/application_package_entity.py +151 -46
  29. snowflake/cli/api/entities/common.py +1 -0
  30. snowflake/cli/api/entities/utils.py +41 -17
  31. snowflake/cli/api/identifiers.py +3 -0
  32. snowflake/cli/api/project/definition.py +11 -0
  33. snowflake/cli/api/project/definition_conversion.py +171 -13
  34. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  35. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  36. snowflake/cli/api/project/schemas/project_definition.py +101 -39
  37. snowflake/cli/api/rendering/project_definition_templates.py +4 -0
  38. snowflake/cli/api/rendering/sql_templates.py +7 -0
  39. snowflake/cli/api/utils/definition_rendering.py +3 -1
  40. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +6 -6
  41. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +44 -42
  42. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  43. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  44. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  45. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -27,6 +27,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
27
27
  from snowflake.cli._plugins.nativeapp.utils import (
28
28
  needs_confirmation,
29
29
  )
30
+ from snowflake.cli._plugins.stage.diff import DiffResult
30
31
  from snowflake.cli._plugins.stage.manager import StageManager
31
32
  from snowflake.cli._plugins.workspace.action_context import ActionContext
32
33
  from snowflake.cli.api.console.abc import AbstractConsole
@@ -48,6 +49,7 @@ from snowflake.cli.api.project.schemas.entities.application_package_entity_model
48
49
  ApplicationPackageEntityModel,
49
50
  )
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
51
53
  from snowflake.cli.api.project.util import extract_schema
52
54
  from snowflake.cli.api.rendering.jinja import (
53
55
  get_basic_jinja_env,
@@ -61,22 +63,16 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
61
63
  A Native App application package.
62
64
  """
63
65
 
64
- def action_bundle(self, ctx: ActionContext):
66
+ def action_bundle(self, ctx: ActionContext, *args, **kwargs):
65
67
  model = self._entity_model
66
- bundle_map = build_bundle(
67
- ctx.project_root, Path(model.deploy_root), model.artifacts
68
- )
69
- bundle_context = BundleContext(
70
- package_name=model.identifier,
71
- artifacts=model.artifacts,
68
+ return self.bundle(
72
69
  project_root=ctx.project_root,
73
- bundle_root=Path(model.bundle_root),
74
70
  deploy_root=Path(model.deploy_root),
71
+ bundle_root=Path(model.bundle_root),
75
72
  generated_root=Path(model.generated_root),
73
+ package_name=model.identifier,
74
+ artifacts=model.artifacts,
76
75
  )
77
- compiler = NativeAppCompiler(bundle_context)
78
- compiler.compile_artifacts()
79
- return bundle_map
80
76
 
81
77
  def action_deploy(
82
78
  self,
@@ -85,7 +81,36 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
85
81
  recursive: bool,
86
82
  paths: List[Path],
87
83
  validate: bool,
84
+ stage_fqn: Optional[str] = None,
85
+ *args,
86
+ **kwargs,
88
87
  ):
88
+ model = self._entity_model
89
+ package_name = model.fqn.identifier
90
+ return self.deploy(
91
+ console=ctx.console,
92
+ project_root=ctx.project_root,
93
+ deploy_root=Path(model.deploy_root),
94
+ bundle_root=Path(model.bundle_root),
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):
89
114
  model = self._entity_model
90
115
  package_name = model.fqn.identifier
91
116
  if model.meta and model.meta.role:
@@ -93,24 +118,121 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
93
118
  else:
94
119
  package_role = ctx.default_role
95
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,
175
+ )
176
+ compiler = NativeAppCompiler(bundle_context)
177
+ compiler.compile_artifacts()
178
+ return bundle_map
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:
96
202
  # 1. Create a bundle
97
- bundle_map = self.action_bundle(ctx)
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
+ )
98
211
 
99
212
  # 2. Create an empty application package, if none exists
100
- self.create_app_package(
101
- console=ctx.console,
213
+ cls.create_app_package(
214
+ console=console,
102
215
  package_name=package_name,
103
216
  package_role=package_role,
104
- package_distribution=model.distribution,
217
+ package_distribution=package_distribution,
105
218
  )
106
219
 
107
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
+
108
231
  # 3. Upload files from deploy root local folder to the above stage
109
- stage_fqn = f"{package_name}.{model.stage}"
110
232
  stage_schema = extract_schema(stage_fqn)
111
- sync_deploy_root_with_stage(
112
- console=ctx.console,
113
- deploy_root=Path(model.deploy_root),
233
+ diff = sync_deploy_root_with_stage(
234
+ console=console,
235
+ deploy_root=deploy_root,
114
236
  package_name=package_name,
115
237
  stage_schema=stage_schema,
116
238
  bundle_map=bundle_map,
@@ -119,21 +241,21 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
119
241
  recursive=recursive,
120
242
  stage_fqn=stage_fqn,
121
243
  local_paths_to_sync=paths,
122
- print_diff=True,
244
+ print_diff=print_diff,
123
245
  )
124
246
 
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,
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,
130
252
  package_name=package_name,
131
- package_warehouse=model.meta.warehouse or ctx.default_warehouse,
253
+ package_warehouse=package_warehouse,
132
254
  )
133
255
 
134
256
  if validate:
135
- self.validate_setup_script(
136
- console=ctx.console,
257
+ cls.validate_setup_script(
258
+ console=console,
137
259
  package_name=package_name,
138
260
  package_role=package_role,
139
261
  stage_fqn=stage_fqn,
@@ -142,24 +264,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
142
264
  deploy_to_scratch_stage_fn=lambda *args: None,
143
265
  )
144
266
 
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
- )
267
+ return diff
163
268
 
164
269
  @staticmethod
165
270
  def get_existing_app_pkg_info(
@@ -9,6 +9,7 @@ class EntityActions(str, Enum):
9
9
  BUNDLE = "action_bundle"
10
10
  DEPLOY = "action_deploy"
11
11
  DROP = "action_drop"
12
+ VALIDATE = "action_validate"
12
13
 
13
14
 
14
15
  T = TypeVar("T")
@@ -40,6 +40,7 @@ from snowflake.cli.api.rendering.sql_templates import (
40
40
  )
41
41
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
42
42
  from snowflake.connector import ProgrammingError
43
+ from snowflake.connector.cursor import SnowflakeCursor
43
44
 
44
45
 
45
46
  def generic_sql_error_handler(
@@ -303,27 +304,34 @@ def render_script_templates(
303
304
  - List of rendered scripts content
304
305
  Size of the return list is the same as the size of the input scripts list.
305
306
  """
306
- scripts_contents = []
307
- for relpath in scripts:
308
- script_full_path = SecurePath(project_root) / relpath
309
- try:
310
- template_content = script_full_path.read_text(file_size_limit_mb=UNLIMITED)
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)
315
- scripts_contents.append(result)
307
+ return [
308
+ render_script_template(project_root, jinja_context, script, override_env)
309
+ for script in scripts
310
+ ]
316
311
 
317
- except FileNotFoundError as e:
318
- raise MissingScriptError(relpath) from e
319
312
 
320
- except jinja2.TemplateSyntaxError as e:
321
- raise InvalidTemplateInFileError(relpath, e, e.lineno) from e
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
322
329
 
323
- except jinja2.UndefinedError as e:
324
- raise InvalidTemplateInFileError(relpath, e) from e
330
+ except jinja2.TemplateSyntaxError as e:
331
+ raise InvalidTemplateInFileError(script, e, e.lineno) from e
325
332
 
326
- return scripts_contents
333
+ except jinja2.UndefinedError as e:
334
+ raise InvalidTemplateInFileError(script, e) from e
327
335
 
328
336
 
329
337
  def validation_item_to_str(item: dict[str, str | int]):
@@ -355,3 +363,19 @@ def drop_generic_object(
355
363
  raise SnowflakeSQLExecutionError(drop_query)
356
364
 
357
365
  console.message(f"Dropped {object_type} {object_name} successfully.")
366
+
367
+
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
377
+
378
+ messages = [row[0] for row in create_or_upgrade_cursor.fetchall()]
379
+ for message in messages:
380
+ console.warning(message)
381
+ console.message("")
@@ -184,3 +184,6 @@ class FQN:
184
184
  from snowflake.cli.api.cli_global_context import get_cli_context
185
185
 
186
186
  return self.using_connection(get_cli_context().connection)
187
+
188
+ def to_dict(self) -> dict:
189
+ return {"name": self.name, "schema": self.schema, "database": self.database}
@@ -23,6 +23,7 @@ from snowflake.cli.api.cli_global_context import get_cli_context
23
23
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
24
24
  from snowflake.cli.api.project.schemas.project_definition import (
25
25
  ProjectProperties,
26
+ YamlOverride,
26
27
  )
27
28
  from snowflake.cli.api.project.util import (
28
29
  append_to_identifier,
@@ -37,6 +38,7 @@ from snowflake.cli.api.utils.definition_rendering import (
37
38
  )
38
39
  from snowflake.cli.api.utils.dict_utils import deep_merge_dicts
39
40
  from snowflake.cli.api.utils.types import Context, Definition
41
+ from yaml import MappingNode, SequenceNode
40
42
 
41
43
  DEFAULT_USERNAME = "unknown_user"
42
44
 
@@ -50,6 +52,7 @@ def _get_merged_definitions(paths: List[Path]) -> Optional[Definition]:
50
52
  loader.add_constructor(
51
53
  yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_duplicates_constructor
52
54
  )
55
+ loader.add_constructor("!override", _override_tag)
53
56
 
54
57
  with spaths[0].open("r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as base_yml:
55
58
  definition = yaml.load(base_yml.read(), Loader=loader) or {}
@@ -113,3 +116,11 @@ def _no_duplicates_constructor(loader, node, deep=False):
113
116
  )
114
117
  mapping[key] = value
115
118
  return loader.construct_mapping(node, deep)
119
+
120
+
121
+ def _override_tag(loader, node, deep=False):
122
+ if isinstance(node, SequenceNode):
123
+ return YamlOverride(data=loader.construct_sequence(node, deep))
124
+ if isinstance(node, MappingNode):
125
+ return YamlOverride(data=loader.construct_mapping(node, deep))
126
+ return node.value
@@ -1,15 +1,31 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from pathlib import Path
3
5
  from typing import Any, Dict, Literal, Optional
4
6
 
5
7
  from click import ClickException
8
+ from snowflake.cli._plugins.nativeapp.artifacts import (
9
+ build_bundle,
10
+ )
6
11
  from snowflake.cli._plugins.snowpark.common import is_name_a_templated_one
7
12
  from snowflake.cli.api.constants import (
8
13
  DEFAULT_ENV_FILE,
9
14
  DEFAULT_PAGES_DIR,
15
+ PROJECT_TEMPLATE_VARIABLE_CLOSING,
10
16
  PROJECT_TEMPLATE_VARIABLE_OPENING,
11
17
  SNOWPARK_SHARED_MIXIN,
12
18
  )
19
+ from snowflake.cli.api.entities.utils import render_script_template
20
+ from snowflake.cli.api.project.schemas.entities.common import (
21
+ SqlScriptHookType,
22
+ )
23
+ from snowflake.cli.api.project.schemas.native_app.application import (
24
+ Application,
25
+ ApplicationV11,
26
+ )
27
+ from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
28
+ from snowflake.cli.api.project.schemas.native_app.package import Package, PackageV11
13
29
  from snowflake.cli.api.project.schemas.project_definition import (
14
30
  ProjectDefinition,
15
31
  ProjectDefinitionV2,
@@ -20,23 +36,31 @@ from snowflake.cli.api.project.schemas.snowpark.callable import (
20
36
  )
21
37
  from snowflake.cli.api.project.schemas.snowpark.snowpark import Snowpark
22
38
  from snowflake.cli.api.project.schemas.streamlit.streamlit import Streamlit
39
+ from snowflake.cli.api.rendering.jinja import get_basic_jinja_env
23
40
 
24
41
  log = logging.getLogger(__name__)
25
42
 
26
43
 
27
44
  def convert_project_definition_to_v2(
28
- pd: ProjectDefinition, accept_templates: bool = False
45
+ project_root: Path, pd: ProjectDefinition, accept_templates: bool = False
29
46
  ) -> ProjectDefinitionV2:
30
47
  _check_if_project_definition_meets_requirements(pd, accept_templates)
31
48
 
32
49
  snowpark_data = convert_snowpark_to_v2_data(pd.snowpark) if pd.snowpark else {}
33
50
  streamlit_data = convert_streamlit_to_v2_data(pd.streamlit) if pd.streamlit else {}
51
+ native_app_data = (
52
+ convert_native_app_to_v2_data(project_root, pd.native_app)
53
+ if pd.native_app
54
+ else {}
55
+ )
34
56
  envs = convert_envs_to_v2(pd)
35
57
 
36
58
  data = {
37
59
  "definition_version": "2",
38
60
  "entities": get_list_of_all_entities(
39
- snowpark_data.get("entities", {}), streamlit_data.get("entities", {})
61
+ snowpark_data.get("entities", {}),
62
+ streamlit_data.get("entities", {}),
63
+ native_app_data.get("entities", {}),
40
64
  ),
41
65
  "mixins": snowpark_data.get("mixins", None),
42
66
  "env": envs,
@@ -99,7 +123,7 @@ def convert_snowpark_to_v2_data(snowpark: Snowpark) -> Dict[str, Any]:
99
123
  return data
100
124
 
101
125
 
102
- def convert_streamlit_to_v2_data(streamlit: Streamlit):
126
+ def convert_streamlit_to_v2_data(streamlit: Streamlit) -> Dict[str, Any]:
103
127
  # Process env file and pages dir
104
128
  environment_file = _process_streamlit_files(streamlit.env_file, "environment")
105
129
  pages_dir = _process_streamlit_files(streamlit.pages_dir, "pages")
@@ -144,6 +168,135 @@ def convert_streamlit_to_v2_data(streamlit: Streamlit):
144
168
  return data
145
169
 
146
170
 
171
+ def convert_native_app_to_v2_data(
172
+ project_root, native_app: NativeApp
173
+ ) -> Dict[str, Any]:
174
+ def _make_meta(obj: Application | Package):
175
+ meta = {}
176
+ if obj.role:
177
+ meta["role"] = obj.role
178
+ if obj.warehouse:
179
+ meta["warehouse"] = obj.warehouse
180
+ if obj.post_deploy:
181
+ meta["post_deploy"] = obj.post_deploy
182
+ return meta
183
+
184
+ def _find_manifest():
185
+ # We don't know which file in the project directory is the actual manifest,
186
+ # and we can't iterate through the artifacts property since the src can contain
187
+ # glob patterns. The simplest solution is to bundle the app and find the
188
+ # manifest file from the resultant BundleMap, since the bundle process ensures
189
+ # that only a single source path can map to the corresponding destination path
190
+ try:
191
+ bundle_map = build_bundle(
192
+ project_root, Path(native_app.deploy_root), native_app.artifacts
193
+ )
194
+ except Exception as e:
195
+ # The manifest field is required, so we can't gracefully handle bundle failures
196
+ raise ClickException(
197
+ f"{e}\nCould not bundle Native App artifacts, unable to perform migration"
198
+ ) from e
199
+
200
+ manifest_path = bundle_map.to_project_path(Path("manifest.yml"))
201
+ if not manifest_path:
202
+ # The manifest field is required, so we can't gracefully handle it being missing
203
+ raise ClickException(
204
+ "manifest.yml file not found in any Native App artifact sources, "
205
+ "unable to perform migration"
206
+ )
207
+
208
+ # Use a POSIX path to be consistent with other migrated fields
209
+ # which use POSIX paths as default values
210
+ return manifest_path.as_posix()
211
+
212
+ def _make_template(template: str) -> str:
213
+ return f"{PROJECT_TEMPLATE_VARIABLE_OPENING} {template} {PROJECT_TEMPLATE_VARIABLE_CLOSING}"
214
+
215
+ def _convert_package_script_files(package_scripts: list[str]):
216
+ # PDFv2 doesn't support package scripts, only post-deploy scripts, so we
217
+ # need to convert the Jinja syntax from {{ }} to <% %>
218
+ # Luckily, package scripts only support {{ package_name }}, so let's convert that tag
219
+ # to v2 template syntax by running it though the template process with a fake
220
+ # package name that's actually a valid v2 template, which will be evaluated
221
+ # when the script is used as a post-deploy script
222
+ fake_package_replacement_template = _make_template(
223
+ f"ctx.entities.{package_entity_name}.identifier"
224
+ )
225
+ jinja_context = dict(package_name=fake_package_replacement_template)
226
+ post_deploy_hooks = []
227
+ for script_file in package_scripts:
228
+ new_contents = render_script_template(
229
+ project_root, jinja_context, script_file, get_basic_jinja_env()
230
+ )
231
+ (project_root / script_file).write_text(new_contents)
232
+ post_deploy_hooks.append(SqlScriptHookType(sql_script=script_file))
233
+ return post_deploy_hooks
234
+
235
+ package_entity_name = "pkg"
236
+ if (
237
+ native_app.package
238
+ and native_app.package.name
239
+ and native_app.package.name != PackageV11.model_fields["name"].default
240
+ ):
241
+ package_identifier = native_app.package.name
242
+ else:
243
+ # Backport the PackageV11 default name template, updated for PDFv2
244
+ package_identifier = _make_template(
245
+ f"fn.concat_ids('{native_app.name}', '_pkg_', fn.sanitize_id(fn.get_username('unknown_user')) | lower)"
246
+ )
247
+ package = {
248
+ "type": "application package",
249
+ "identifier": package_identifier,
250
+ "manifest": _find_manifest(),
251
+ "artifacts": native_app.artifacts,
252
+ "bundle_root": native_app.bundle_root,
253
+ "generated_root": native_app.generated_root,
254
+ "deploy_root": native_app.deploy_root,
255
+ "stage": native_app.source_stage,
256
+ "scratch_stage": native_app.scratch_stage,
257
+ }
258
+ if native_app.package:
259
+ package["distribution"] = native_app.package.distribution
260
+ package_meta = _make_meta(native_app.package)
261
+ if native_app.package.scripts:
262
+ converted_post_deploy_hooks = _convert_package_script_files(
263
+ native_app.package.scripts
264
+ )
265
+ package_meta["post_deploy"] = (
266
+ package_meta.get("post_deploy", []) + converted_post_deploy_hooks
267
+ )
268
+ if package_meta:
269
+ package["meta"] = package_meta
270
+
271
+ app_entity_name = "app"
272
+ if (
273
+ native_app.application
274
+ and native_app.application.name
275
+ and native_app.application.name != ApplicationV11.model_fields["name"].default
276
+ ):
277
+ app_identifier = native_app.application.name
278
+ else:
279
+ # Backport the ApplicationV11 default name template, updated for PDFv2
280
+ app_identifier = _make_template(
281
+ f"fn.concat_ids('{native_app.name}', '_', fn.sanitize_id(fn.get_username('unknown_user')) | lower)"
282
+ )
283
+ app = {
284
+ "type": "application",
285
+ "identifier": app_identifier,
286
+ "from": {"target": package_entity_name},
287
+ }
288
+ if native_app.application:
289
+ if app_meta := _make_meta(native_app.application):
290
+ app["meta"] = app_meta
291
+
292
+ return {
293
+ "entities": {
294
+ package_entity_name: package,
295
+ app_entity_name: app,
296
+ }
297
+ }
298
+
299
+
147
300
  def convert_envs_to_v2(pd: ProjectDefinition):
148
301
  if hasattr(pd, "env") and pd.env:
149
302
  data = {k: v for k, v in pd.env.items()}
@@ -166,10 +319,6 @@ def _check_if_project_definition_meets_requirements(
166
319
  log.warning(
167
320
  "Your V1 definition contains templates. We cannot guarantee the correctness of the migration."
168
321
  )
169
- if pd.native_app:
170
- raise ClickException(
171
- "Your project file contains a native app definition. Conversion of Native apps is not yet supported"
172
- )
173
322
 
174
323
 
175
324
  def _process_streamlit_files(
@@ -185,10 +334,19 @@ def _process_streamlit_files(
185
334
 
186
335
 
187
336
  def get_list_of_all_entities(
188
- snowpark_entities: Dict[str, Any], streamlit_entities: Dict[str, Any]
337
+ snowpark_entities: Dict[str, Any],
338
+ streamlit_entities: Dict[str, Any],
339
+ native_app_entities: Dict[str, Any],
189
340
  ):
190
- if snowpark_entities.keys() & streamlit_entities.keys():
191
- raise ClickException(
192
- "In your project, streamlit and snowpark entities share the same name. Please rename them and try again."
193
- )
194
- return snowpark_entities | streamlit_entities
341
+ # Check all combinations of entity types for overlapping names
342
+ # (No need to use itertools here, PDFv1 only supports these three types)
343
+ for types, first, second in [
344
+ ("streamlit and snowpark", streamlit_entities, snowpark_entities),
345
+ ("streamlit and native app", streamlit_entities, native_app_entities),
346
+ ("native app and snowpark", native_app_entities, snowpark_entities),
347
+ ]:
348
+ if first.keys() & second.keys():
349
+ raise ClickException(
350
+ f"In your project, {types} entities share the same name. Please rename them and try again."
351
+ )
352
+ return snowpark_entities | streamlit_entities | native_app_entities
@@ -61,18 +61,6 @@ class MetaField(UpdatableModel):
61
61
  return mixins
62
62
 
63
63
 
64
- class DefaultsField(UpdatableModel):
65
- schema_: Optional[str] = Field(
66
- title="Schema.",
67
- alias="schema",
68
- default=None,
69
- )
70
- stage: Optional[str] = Field(
71
- title="Stage.",
72
- default=None,
73
- )
74
-
75
-
76
64
  class EntityModelBase(ABC, UpdatableModel):
77
65
  @classmethod
78
66
  def get_type(cls) -> str:
@@ -25,8 +25,8 @@ from snowflake.cli.api.project.schemas.updatable_model import (
25
25
 
26
26
  class Identifier(UpdatableModel):
27
27
  name: str = Field(title="Entity name")
28
- schema_: str = Field(title="Entity schema", alias="schema", default=None)
29
- database: str = Field(title="Entity database", default=None)
28
+ schema_: Optional[str] = Field(title="Entity schema", alias="schema", default=None)
29
+ database: Optional[str] = Field(title="Entity database", default=None)
30
30
 
31
31
 
32
32
  class ObjectIdentifierBaseModel: