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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +10 -1
- snowflake/cli/_app/snow_connector.py +76 -29
- snowflake/cli/_app/telemetry.py +8 -4
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/_plugins/git/commands.py +55 -14
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -5
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +49 -31
- snowflake/cli/_plugins/nativeapp/manager.py +46 -87
- snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +9 -152
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
- snowflake/cli/_plugins/snowpark/commands.py +1 -1
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/_plugins/streamlit/commands.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +9 -0
- snowflake/cli/_plugins/workspace/action_context.py +2 -1
- snowflake/cli/_plugins/workspace/commands.py +48 -16
- snowflake/cli/_plugins/workspace/manager.py +1 -0
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/flags.py +76 -91
- snowflake/cli/api/commands/snow_typer.py +6 -4
- snowflake/cli/api/config.py +1 -1
- snowflake/cli/api/connections.py +214 -0
- snowflake/cli/api/console/abc.py +4 -2
- snowflake/cli/api/entities/application_entity.py +687 -2
- snowflake/cli/api/entities/application_package_entity.py +151 -46
- snowflake/cli/api/entities/common.py +1 -0
- snowflake/cli/api/entities/utils.py +41 -17
- snowflake/cli/api/identifiers.py +3 -0
- snowflake/cli/api/project/definition.py +11 -0
- snowflake/cli/api/project/definition_conversion.py +171 -13
- snowflake/cli/api/project/schemas/entities/common.py +0 -12
- snowflake/cli/api/project/schemas/identifier_model.py +2 -2
- snowflake/cli/api/project/schemas/project_definition.py +101 -39
- snowflake/cli/api/rendering/project_definition_templates.py +4 -0
- snowflake/cli/api/rendering/sql_templates.py +7 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -1
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +6 -6
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +44 -42
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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 =
|
|
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
|
-
|
|
101
|
-
console=
|
|
213
|
+
cls.create_app_package(
|
|
214
|
+
console=console,
|
|
102
215
|
package_name=package_name,
|
|
103
216
|
package_role=package_role,
|
|
104
|
-
package_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=
|
|
113
|
-
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=
|
|
244
|
+
print_diff=print_diff,
|
|
123
245
|
)
|
|
124
246
|
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
console=
|
|
128
|
-
project_root=
|
|
129
|
-
post_deploy_hooks=
|
|
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=
|
|
253
|
+
package_warehouse=package_warehouse,
|
|
132
254
|
)
|
|
133
255
|
|
|
134
256
|
if validate:
|
|
135
|
-
|
|
136
|
-
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
|
-
|
|
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(
|
|
@@ -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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
330
|
+
except jinja2.TemplateSyntaxError as e:
|
|
331
|
+
raise InvalidTemplateInFileError(script, e, e.lineno) from e
|
|
325
332
|
|
|
326
|
-
|
|
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("")
|
snowflake/cli/api/identifiers.py
CHANGED
|
@@ -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", {}),
|
|
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],
|
|
337
|
+
snowpark_entities: Dict[str, Any],
|
|
338
|
+
streamlit_entities: Dict[str, Any],
|
|
339
|
+
native_app_entities: Dict[str, Any],
|
|
189
340
|
):
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
|
|
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:
|