snowflake-cli-labs 3.0.0rc0__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 +91 -37
- snowflake/cli/_app/telemetry.py +8 -4
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/_plugins/connection/commands.py +3 -2
- snowflake/cli/_plugins/git/commands.py +55 -14
- snowflake/cli/_plugins/git/manager.py +14 -6
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
- snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
- snowflake/cli/_plugins/nativeapp/manager.py +74 -144
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
- 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 +17 -246
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
- snowflake/cli/_plugins/snowpark/commands.py +5 -65
- snowflake/cli/_plugins/snowpark/common.py +17 -1
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
- snowflake/cli/_plugins/sql/commands.py +1 -2
- snowflake/cli/_plugins/stage/commands.py +2 -2
- snowflake/cli/_plugins/stage/manager.py +46 -15
- snowflake/cli/_plugins/streamlit/commands.py +4 -63
- snowflake/cli/_plugins/streamlit/manager.py +13 -0
- snowflake/cli/_plugins/workspace/action_context.py +7 -0
- snowflake/cli/_plugins/workspace/commands.py +145 -32
- snowflake/cli/_plugins/workspace/manager.py +21 -4
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/decorators.py +1 -1
- snowflake/cli/api/commands/flags.py +106 -102
- snowflake/cli/api/commands/snow_typer.py +15 -6
- snowflake/cli/api/config.py +18 -5
- snowflake/cli/api/connections.py +214 -0
- snowflake/cli/api/console/abc.py +4 -2
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/application_entity.py +687 -2
- snowflake/cli/api/entities/application_package_entity.py +407 -9
- snowflake/cli/api/entities/common.py +7 -2
- snowflake/cli/api/entities/utils.py +80 -20
- snowflake/cli/api/exceptions.py +12 -2
- snowflake/cli/api/feature_flags.py +0 -2
- snowflake/cli/api/identifiers.py +3 -0
- snowflake/cli/api/project/definition.py +35 -1
- snowflake/cli/api/project/definition_conversion.py +352 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
- 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 +102 -43
- snowflake/cli/api/rendering/jinja.py +2 -16
- snowflake/cli/api/rendering/project_definition_templates.py +5 -1
- snowflake/cli/api/rendering/sql_templates.py +14 -4
- snowflake/cli/api/secure_path.py +13 -18
- snowflake/cli/api/secure_utils.py +90 -1
- snowflake/cli/api/sql_execution.py +13 -0
- snowflake/cli/api/utils/definition_rendering.py +7 -7
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from contextlib import contextmanager
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from textwrap import dedent
|
|
4
|
-
from typing import List, Optional
|
|
5
|
+
from typing import Callable, List, Optional
|
|
5
6
|
|
|
7
|
+
import typer
|
|
6
8
|
from click import ClickException
|
|
7
9
|
from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
|
|
8
10
|
from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
|
|
@@ -10,29 +12,50 @@ from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler
|
|
|
10
12
|
from snowflake.cli._plugins.nativeapp.constants import (
|
|
11
13
|
ALLOWED_SPECIAL_COMMENTS,
|
|
12
14
|
COMMENT_COL,
|
|
15
|
+
EXTERNAL_DISTRIBUTION,
|
|
13
16
|
INTERNAL_DISTRIBUTION,
|
|
14
17
|
NAME_COL,
|
|
18
|
+
OWNER_COL,
|
|
15
19
|
SPECIAL_COMMENT,
|
|
16
20
|
)
|
|
17
21
|
from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
18
22
|
ApplicationPackageAlreadyExistsError,
|
|
23
|
+
ApplicationPackageDoesNotExistError,
|
|
24
|
+
CouldNotDropApplicationPackageWithVersions,
|
|
25
|
+
SetupScriptFailedValidation,
|
|
19
26
|
)
|
|
27
|
+
from snowflake.cli._plugins.nativeapp.utils import (
|
|
28
|
+
needs_confirmation,
|
|
29
|
+
)
|
|
30
|
+
from snowflake.cli._plugins.stage.diff import DiffResult
|
|
31
|
+
from snowflake.cli._plugins.stage.manager import StageManager
|
|
20
32
|
from snowflake.cli._plugins.workspace.action_context import ActionContext
|
|
21
33
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
22
34
|
from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
|
|
23
35
|
from snowflake.cli.api.entities.utils import (
|
|
36
|
+
drop_generic_object,
|
|
24
37
|
ensure_correct_owner,
|
|
38
|
+
execute_post_deploy_hooks,
|
|
25
39
|
generic_sql_error_handler,
|
|
26
40
|
render_script_templates,
|
|
41
|
+
sync_deploy_root_with_stage,
|
|
42
|
+
validation_item_to_str,
|
|
43
|
+
)
|
|
44
|
+
from snowflake.cli.api.errno import (
|
|
45
|
+
DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
|
|
27
46
|
)
|
|
28
47
|
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
|
|
29
48
|
from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
|
|
30
49
|
ApplicationPackageEntityModel,
|
|
31
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
|
|
53
|
+
from snowflake.cli.api.project.util import extract_schema
|
|
32
54
|
from snowflake.cli.api.rendering.jinja import (
|
|
33
|
-
|
|
55
|
+
get_basic_jinja_env,
|
|
34
56
|
)
|
|
35
57
|
from snowflake.connector import ProgrammingError
|
|
58
|
+
from snowflake.connector.cursor import DictCursor
|
|
36
59
|
|
|
37
60
|
|
|
38
61
|
class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
@@ -40,23 +63,209 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
40
63
|
A Native App application package.
|
|
41
64
|
"""
|
|
42
65
|
|
|
43
|
-
def action_bundle(self, ctx: ActionContext):
|
|
66
|
+
def action_bundle(self, ctx: ActionContext, *args, **kwargs):
|
|
44
67
|
model = self._entity_model
|
|
45
|
-
|
|
46
|
-
ctx.project_root,
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
return self.bundle(
|
|
69
|
+
project_root=ctx.project_root,
|
|
70
|
+
deploy_root=Path(model.deploy_root),
|
|
71
|
+
bundle_root=Path(model.bundle_root),
|
|
72
|
+
generated_root=Path(model.generated_root),
|
|
49
73
|
package_name=model.identifier,
|
|
50
74
|
artifacts=model.artifacts,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def action_deploy(
|
|
78
|
+
self,
|
|
79
|
+
ctx: ActionContext,
|
|
80
|
+
prune: bool,
|
|
81
|
+
recursive: bool,
|
|
82
|
+
paths: List[Path],
|
|
83
|
+
validate: bool,
|
|
84
|
+
stage_fqn: Optional[str] = None,
|
|
85
|
+
*args,
|
|
86
|
+
**kwargs,
|
|
87
|
+
):
|
|
88
|
+
model = self._entity_model
|
|
89
|
+
package_name = model.fqn.identifier
|
|
90
|
+
return self.deploy(
|
|
91
|
+
console=ctx.console,
|
|
51
92
|
project_root=ctx.project_root,
|
|
52
|
-
bundle_root=Path(model.bundle_root),
|
|
53
93
|
deploy_root=Path(model.deploy_root),
|
|
94
|
+
bundle_root=Path(model.bundle_root),
|
|
54
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):
|
|
114
|
+
model = self._entity_model
|
|
115
|
+
package_name = model.fqn.identifier
|
|
116
|
+
if model.meta and model.meta.role:
|
|
117
|
+
package_role = model.meta.role
|
|
118
|
+
else:
|
|
119
|
+
package_role = ctx.default_role
|
|
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,
|
|
55
175
|
)
|
|
56
176
|
compiler = NativeAppCompiler(bundle_context)
|
|
57
177
|
compiler.compile_artifacts()
|
|
58
178
|
return bundle_map
|
|
59
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:
|
|
202
|
+
# 1. Create a bundle
|
|
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
|
+
)
|
|
211
|
+
|
|
212
|
+
# 2. Create an empty application package, if none exists
|
|
213
|
+
cls.create_app_package(
|
|
214
|
+
console=console,
|
|
215
|
+
package_name=package_name,
|
|
216
|
+
package_role=package_role,
|
|
217
|
+
package_distribution=package_distribution,
|
|
218
|
+
)
|
|
219
|
+
|
|
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
|
+
|
|
231
|
+
# 3. Upload files from deploy root local folder to the above stage
|
|
232
|
+
stage_schema = extract_schema(stage_fqn)
|
|
233
|
+
diff = sync_deploy_root_with_stage(
|
|
234
|
+
console=console,
|
|
235
|
+
deploy_root=deploy_root,
|
|
236
|
+
package_name=package_name,
|
|
237
|
+
stage_schema=stage_schema,
|
|
238
|
+
bundle_map=bundle_map,
|
|
239
|
+
role=package_role,
|
|
240
|
+
prune=prune,
|
|
241
|
+
recursive=recursive,
|
|
242
|
+
stage_fqn=stage_fqn,
|
|
243
|
+
local_paths_to_sync=paths,
|
|
244
|
+
print_diff=print_diff,
|
|
245
|
+
)
|
|
246
|
+
|
|
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,
|
|
252
|
+
package_name=package_name,
|
|
253
|
+
package_warehouse=package_warehouse,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if validate:
|
|
257
|
+
cls.validate_setup_script(
|
|
258
|
+
console=console,
|
|
259
|
+
package_name=package_name,
|
|
260
|
+
package_role=package_role,
|
|
261
|
+
stage_fqn=stage_fqn,
|
|
262
|
+
use_scratch_stage=False,
|
|
263
|
+
scratch_stage_fqn="",
|
|
264
|
+
deploy_to_scratch_stage_fn=lambda *args: None,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return diff
|
|
268
|
+
|
|
60
269
|
@staticmethod
|
|
61
270
|
def get_existing_app_pkg_info(
|
|
62
271
|
package_name: str,
|
|
@@ -178,9 +387,9 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
178
387
|
|
|
179
388
|
queued_queries = render_script_templates(
|
|
180
389
|
project_root,
|
|
181
|
-
jinja_render_from_str,
|
|
182
390
|
dict(package_name=package_name),
|
|
183
391
|
package_scripts,
|
|
392
|
+
get_basic_jinja_env(),
|
|
184
393
|
)
|
|
185
394
|
|
|
186
395
|
# once we're sure all the templates expanded correctly, execute all of them
|
|
@@ -258,3 +467,192 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
258
467
|
"""
|
|
259
468
|
)
|
|
260
469
|
)
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
def execute_post_deploy_hooks(
|
|
473
|
+
cls,
|
|
474
|
+
console: AbstractConsole,
|
|
475
|
+
project_root: Path,
|
|
476
|
+
post_deploy_hooks: Optional[List[PostDeployHook]],
|
|
477
|
+
package_name: str,
|
|
478
|
+
package_warehouse: Optional[str],
|
|
479
|
+
):
|
|
480
|
+
with cls.use_package_warehouse(package_warehouse):
|
|
481
|
+
execute_post_deploy_hooks(
|
|
482
|
+
console=console,
|
|
483
|
+
project_root=project_root,
|
|
484
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
485
|
+
deployed_object_type="application package",
|
|
486
|
+
database_name=package_name,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def validate_setup_script(
|
|
491
|
+
cls,
|
|
492
|
+
console: AbstractConsole,
|
|
493
|
+
package_name: str,
|
|
494
|
+
package_role: str,
|
|
495
|
+
stage_fqn: str,
|
|
496
|
+
use_scratch_stage: bool,
|
|
497
|
+
scratch_stage_fqn: str,
|
|
498
|
+
deploy_to_scratch_stage_fn: Callable,
|
|
499
|
+
):
|
|
500
|
+
"""Validates Native App setup script SQL."""
|
|
501
|
+
with console.phase(f"Validating Snowflake Native App setup script."):
|
|
502
|
+
validation_result = cls.get_validation_result(
|
|
503
|
+
console=console,
|
|
504
|
+
package_name=package_name,
|
|
505
|
+
package_role=package_role,
|
|
506
|
+
stage_fqn=stage_fqn,
|
|
507
|
+
use_scratch_stage=use_scratch_stage,
|
|
508
|
+
scratch_stage_fqn=scratch_stage_fqn,
|
|
509
|
+
deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# First print warnings, regardless of the outcome of validation
|
|
513
|
+
for warning in validation_result.get("warnings", []):
|
|
514
|
+
console.warning(validation_item_to_str(warning))
|
|
515
|
+
|
|
516
|
+
# Then print errors
|
|
517
|
+
for error in validation_result.get("errors", []):
|
|
518
|
+
# Print them as warnings for now since we're going to be
|
|
519
|
+
# revamping CLI output soon
|
|
520
|
+
console.warning(validation_item_to_str(error))
|
|
521
|
+
|
|
522
|
+
# Then raise an exception if validation failed
|
|
523
|
+
if validation_result["status"] == "FAIL":
|
|
524
|
+
raise SetupScriptFailedValidation()
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def get_validation_result(
|
|
528
|
+
console: AbstractConsole,
|
|
529
|
+
package_name: str,
|
|
530
|
+
package_role: str,
|
|
531
|
+
stage_fqn: str,
|
|
532
|
+
use_scratch_stage: bool,
|
|
533
|
+
scratch_stage_fqn: str,
|
|
534
|
+
deploy_to_scratch_stage_fn: Callable,
|
|
535
|
+
):
|
|
536
|
+
"""Call system$validate_native_app_setup() to validate deployed Native App setup script."""
|
|
537
|
+
if use_scratch_stage:
|
|
538
|
+
stage_fqn = scratch_stage_fqn
|
|
539
|
+
deploy_to_scratch_stage_fn()
|
|
540
|
+
prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
|
|
541
|
+
sql_executor = get_sql_executor()
|
|
542
|
+
try:
|
|
543
|
+
cursor = sql_executor.execute_query(
|
|
544
|
+
f"call system$validate_native_app_setup('{prefixed_stage_fqn}')"
|
|
545
|
+
)
|
|
546
|
+
except ProgrammingError as err:
|
|
547
|
+
if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
|
|
548
|
+
raise ApplicationPackageDoesNotExistError(package_name)
|
|
549
|
+
generic_sql_error_handler(err)
|
|
550
|
+
else:
|
|
551
|
+
if not cursor.rowcount:
|
|
552
|
+
raise SnowflakeSQLExecutionError()
|
|
553
|
+
return json.loads(cursor.fetchone()[0])
|
|
554
|
+
finally:
|
|
555
|
+
if use_scratch_stage:
|
|
556
|
+
console.step(f"Dropping stage {scratch_stage_fqn}.")
|
|
557
|
+
with sql_executor.use_role(package_role):
|
|
558
|
+
sql_executor.execute_query(
|
|
559
|
+
f"drop stage if exists {scratch_stage_fqn}"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
@classmethod
|
|
563
|
+
def drop(
|
|
564
|
+
cls,
|
|
565
|
+
console: AbstractConsole,
|
|
566
|
+
package_name: str,
|
|
567
|
+
package_role: str,
|
|
568
|
+
force_drop: bool,
|
|
569
|
+
):
|
|
570
|
+
sql_executor = get_sql_executor()
|
|
571
|
+
needs_confirm = True
|
|
572
|
+
|
|
573
|
+
# 1. If existing application package is not found, exit gracefully
|
|
574
|
+
show_obj_row = cls.get_existing_app_pkg_info(
|
|
575
|
+
package_name=package_name,
|
|
576
|
+
package_role=package_role,
|
|
577
|
+
)
|
|
578
|
+
if show_obj_row is None:
|
|
579
|
+
console.warning(
|
|
580
|
+
f"Role {package_role} does not own any application package with the name {package_name}, or the application package does not exist."
|
|
581
|
+
)
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
# 2. Check for the right owner
|
|
585
|
+
ensure_correct_owner(row=show_obj_row, role=package_role, obj_name=package_name)
|
|
586
|
+
|
|
587
|
+
with sql_executor.use_role(package_role):
|
|
588
|
+
# 3. Check for versions in the application package
|
|
589
|
+
show_versions_query = f"show versions in application package {package_name}"
|
|
590
|
+
show_versions_cursor = sql_executor.execute_query(
|
|
591
|
+
show_versions_query, cursor_class=DictCursor
|
|
592
|
+
)
|
|
593
|
+
if show_versions_cursor.rowcount is None:
|
|
594
|
+
raise SnowflakeSQLExecutionError(show_versions_query)
|
|
595
|
+
|
|
596
|
+
if show_versions_cursor.rowcount > 0:
|
|
597
|
+
# allow dropping a package with versions when --force is set
|
|
598
|
+
if not force_drop:
|
|
599
|
+
raise CouldNotDropApplicationPackageWithVersions(
|
|
600
|
+
"Drop versions first, or use --force to override."
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# 4. Check distribution of the existing application package
|
|
604
|
+
actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
|
|
605
|
+
package_name=package_name,
|
|
606
|
+
package_role=package_role,
|
|
607
|
+
)
|
|
608
|
+
if not cls.verify_project_distribution(
|
|
609
|
+
console=console,
|
|
610
|
+
package_name=package_name,
|
|
611
|
+
package_role=package_role,
|
|
612
|
+
package_distribution=actual_distribution,
|
|
613
|
+
):
|
|
614
|
+
console.warning(
|
|
615
|
+
f"Dropping application package {package_name} with distribution '{actual_distribution}'."
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# 5. If distribution is internal, check if created by the Snowflake CLI
|
|
619
|
+
row_comment = show_obj_row[COMMENT_COL]
|
|
620
|
+
if actual_distribution == INTERNAL_DISTRIBUTION:
|
|
621
|
+
if row_comment in ALLOWED_SPECIAL_COMMENTS:
|
|
622
|
+
needs_confirm = False
|
|
623
|
+
else:
|
|
624
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
625
|
+
console.warning(
|
|
626
|
+
f"Application package {package_name} was not created by Snowflake CLI."
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
630
|
+
console.warning(
|
|
631
|
+
f"Application package {package_name} in your Snowflake account has distribution property '{EXTERNAL_DISTRIBUTION}' and could be associated with one or more of your listings on Snowflake Marketplace."
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
635
|
+
should_drop_object = typer.confirm(
|
|
636
|
+
dedent(
|
|
637
|
+
f"""\
|
|
638
|
+
Application package details:
|
|
639
|
+
Name: {package_name}
|
|
640
|
+
Created on: {show_obj_row["created_on"]}
|
|
641
|
+
Distribution: {actual_distribution}
|
|
642
|
+
Owner: {show_obj_row[OWNER_COL]}
|
|
643
|
+
Comment: {show_obj_row[COMMENT_COL]}
|
|
644
|
+
Are you sure you want to drop it?
|
|
645
|
+
"""
|
|
646
|
+
)
|
|
647
|
+
)
|
|
648
|
+
if not should_drop_object:
|
|
649
|
+
console.message(f"Did not drop application package {package_name}.")
|
|
650
|
+
return # The user desires to keep the application package, therefore exit gracefully
|
|
651
|
+
|
|
652
|
+
# All validations have passed, drop object
|
|
653
|
+
drop_generic_object(
|
|
654
|
+
console=console,
|
|
655
|
+
object_type="application package",
|
|
656
|
+
object_name=package_name,
|
|
657
|
+
role=package_role,
|
|
658
|
+
)
|
|
@@ -7,6 +7,9 @@ from snowflake.cli.api.sql_execution import SqlExecutor
|
|
|
7
7
|
|
|
8
8
|
class EntityActions(str, Enum):
|
|
9
9
|
BUNDLE = "action_bundle"
|
|
10
|
+
DEPLOY = "action_deploy"
|
|
11
|
+
DROP = "action_drop"
|
|
12
|
+
VALIDATE = "action_validate"
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
T = TypeVar("T")
|
|
@@ -35,11 +38,13 @@ class EntityBase(Generic[T]):
|
|
|
35
38
|
"""
|
|
36
39
|
return callable(getattr(self, action, None))
|
|
37
40
|
|
|
38
|
-
def perform(
|
|
41
|
+
def perform(
|
|
42
|
+
self, action: EntityActions, action_ctx: ActionContext, *args, **kwargs
|
|
43
|
+
):
|
|
39
44
|
"""
|
|
40
45
|
Performs the requested action.
|
|
41
46
|
"""
|
|
42
|
-
return getattr(self, action)(action_ctx)
|
|
47
|
+
return getattr(self, action)(action_ctx, *args, **kwargs)
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
def get_sql_executor() -> SqlExecutor:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from textwrap import dedent
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, List, NoReturn, Optional
|
|
5
5
|
|
|
6
6
|
import jinja2
|
|
7
7
|
from click import ClickException
|
|
@@ -11,7 +11,7 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
|
|
|
11
11
|
)
|
|
12
12
|
from snowflake.cli._plugins.nativeapp.constants import OWNER_COL
|
|
13
13
|
from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
14
|
-
|
|
14
|
+
InvalidTemplateInFileError,
|
|
15
15
|
MissingScriptError,
|
|
16
16
|
UnexpectedOwnerError,
|
|
17
17
|
)
|
|
@@ -25,19 +25,22 @@ from snowflake.cli._plugins.stage.diff import (
|
|
|
25
25
|
to_stage_path,
|
|
26
26
|
)
|
|
27
27
|
from snowflake.cli._plugins.stage.utils import print_diff_to_console
|
|
28
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
28
29
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
29
30
|
from snowflake.cli.api.entities.common import get_sql_executor
|
|
30
31
|
from snowflake.cli.api.errno import (
|
|
31
32
|
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
|
|
32
33
|
NO_WAREHOUSE_SELECTED_IN_SESSION,
|
|
33
34
|
)
|
|
35
|
+
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
|
|
34
36
|
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
|
|
35
37
|
from snowflake.cli.api.project.util import unquote_identifier
|
|
36
38
|
from snowflake.cli.api.rendering.sql_templates import (
|
|
37
|
-
|
|
39
|
+
choose_sql_jinja_env_based_on_template_syntax,
|
|
38
40
|
)
|
|
39
41
|
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
40
42
|
from snowflake.connector import ProgrammingError
|
|
43
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
def generic_sql_error_handler(
|
|
@@ -272,8 +275,7 @@ def execute_post_deploy_hooks(
|
|
|
272
275
|
|
|
273
276
|
scripts_content_list = render_script_templates(
|
|
274
277
|
project_root,
|
|
275
|
-
|
|
276
|
-
{},
|
|
278
|
+
get_cli_context().template_context,
|
|
277
279
|
sql_scripts_paths,
|
|
278
280
|
)
|
|
279
281
|
|
|
@@ -287,35 +289,93 @@ def execute_post_deploy_hooks(
|
|
|
287
289
|
|
|
288
290
|
def render_script_templates(
|
|
289
291
|
project_root: Path,
|
|
290
|
-
render_from_str: Callable[[str, Dict[str, Any]], str],
|
|
291
292
|
jinja_context: dict[str, Any],
|
|
292
293
|
scripts: List[str],
|
|
294
|
+
override_env: Optional[jinja2.Environment] = None,
|
|
293
295
|
) -> List[str]:
|
|
294
296
|
"""
|
|
295
297
|
Input:
|
|
296
298
|
- project_root: path to project root
|
|
297
|
-
- render_from_str: function which renders a jinja template from a string and jinja context
|
|
298
299
|
- jinja_context: a dictionary with the jinja context
|
|
299
300
|
- scripts: list of script paths relative to the project root
|
|
301
|
+
- override_env: optional jinja environment to use for rendering,
|
|
302
|
+
if not provided, the environment will be chosen based on the template syntax
|
|
300
303
|
Returns:
|
|
301
304
|
- List of rendered scripts content
|
|
302
305
|
Size of the return list is the same as the size of the input scripts list.
|
|
303
306
|
"""
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
return [
|
|
308
|
+
render_script_template(project_root, jinja_context, script, override_env)
|
|
309
|
+
for script in scripts
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
|
|
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
|
|
329
|
+
|
|
330
|
+
except jinja2.TemplateSyntaxError as e:
|
|
331
|
+
raise InvalidTemplateInFileError(script, e, e.lineno) from e
|
|
332
|
+
|
|
333
|
+
except jinja2.UndefinedError as e:
|
|
334
|
+
raise InvalidTemplateInFileError(script, e) from e
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def validation_item_to_str(item: dict[str, str | int]):
|
|
338
|
+
s = item["message"]
|
|
339
|
+
if item["errorCode"]:
|
|
340
|
+
s = f"{s} (error code {item['errorCode']})"
|
|
341
|
+
return s
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def drop_generic_object(
|
|
345
|
+
console: AbstractConsole,
|
|
346
|
+
object_type: str,
|
|
347
|
+
object_name: str,
|
|
348
|
+
role: str,
|
|
349
|
+
cascade: bool = False,
|
|
350
|
+
):
|
|
351
|
+
"""
|
|
352
|
+
Drop object using the given role.
|
|
353
|
+
"""
|
|
354
|
+
sql_executor = get_sql_executor()
|
|
355
|
+
with sql_executor.use_role(role):
|
|
356
|
+
console.step(f"Dropping {object_type} {object_name} now.")
|
|
357
|
+
drop_query = f"drop {object_type} {object_name}"
|
|
358
|
+
if cascade:
|
|
359
|
+
drop_query += " cascade"
|
|
307
360
|
try:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
361
|
+
sql_executor.execute_query(drop_query)
|
|
362
|
+
except:
|
|
363
|
+
raise SnowflakeSQLExecutionError(drop_query)
|
|
311
364
|
|
|
312
|
-
|
|
313
|
-
raise MissingScriptError(relpath) from e
|
|
365
|
+
console.message(f"Dropped {object_type} {object_name} successfully.")
|
|
314
366
|
|
|
315
|
-
except jinja2.TemplateSyntaxError as e:
|
|
316
|
-
raise InvalidScriptError(relpath, e, e.lineno) from e
|
|
317
367
|
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
320
377
|
|
|
321
|
-
|
|
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/exceptions.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import Optional
|
|
|
19
19
|
|
|
20
20
|
from click.exceptions import ClickException, UsageError
|
|
21
21
|
from snowflake.cli.api.constants import ObjectType
|
|
22
|
+
from snowflake.connector.compat import IS_WINDOWS
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class EnvironmentVariableNotFoundError(ClickException):
|
|
@@ -140,9 +141,18 @@ class DirectoryIsNotEmptyError(ClickException):
|
|
|
140
141
|
|
|
141
142
|
class ConfigFileTooWidePermissionsError(ClickException):
|
|
142
143
|
def __init__(self, path: Path):
|
|
143
|
-
|
|
144
|
-
f'
|
|
144
|
+
change_permissons_command = (
|
|
145
|
+
f'icacls "{path}" /deny <USER_ID>:F'
|
|
146
|
+
if IS_WINDOWS
|
|
147
|
+
else f'chmod 0600 "{path}"'
|
|
145
148
|
)
|
|
149
|
+
msg = f"Configuration file {path} has too wide permissions, run `{change_permissons_command}`."
|
|
150
|
+
if IS_WINDOWS:
|
|
151
|
+
msg += (
|
|
152
|
+
f'\nTo check which users have access to the file run `icacls "{path}"`.'
|
|
153
|
+
"Run the above command for all users except you and administrators."
|
|
154
|
+
)
|
|
155
|
+
super().__init__(msg)
|
|
146
156
|
|
|
147
157
|
|
|
148
158
|
class DatabaseNotProvidedError(ClickException):
|