snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc1__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/snow_connector.py +18 -11
- snowflake/cli/_plugins/connection/commands.py +3 -2
- 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 +4 -6
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
- snowflake/cli/_plugins/nativeapp/manager.py +29 -58
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
- snowflake/cli/_plugins/snowpark/commands.py +5 -65
- snowflake/cli/_plugins/snowpark/common.py +17 -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 +4 -0
- snowflake/cli/_plugins/workspace/action_context.py +6 -0
- snowflake/cli/_plugins/workspace/commands.py +103 -22
- snowflake/cli/_plugins/workspace/manager.py +20 -4
- snowflake/cli/api/cli_global_context.py +6 -6
- snowflake/cli/api/commands/decorators.py +1 -1
- snowflake/cli/api/commands/flags.py +31 -12
- snowflake/cli/api/commands/snow_typer.py +9 -2
- snowflake/cli/api/config.py +17 -4
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/application_package_entity.py +296 -3
- snowflake/cli/api/entities/common.py +6 -2
- snowflake/cli/api/entities/utils.py +46 -10
- snowflake/cli/api/exceptions.py +12 -2
- snowflake/cli/api/feature_flags.py +0 -2
- snowflake/cli/api/project/definition.py +24 -1
- snowflake/cli/api/project/definition_conversion.py +194 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
- snowflake/cli/api/project/schemas/project_definition.py +1 -4
- snowflake/cli/api/rendering/jinja.py +2 -16
- snowflake/cli/api/rendering/project_definition_templates.py +1 -1
- snowflake/cli/api/rendering/sql_templates.py +7 -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 +4 -6
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.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,48 @@ 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.manager import StageManager
|
|
20
31
|
from snowflake.cli._plugins.workspace.action_context import ActionContext
|
|
21
32
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
22
33
|
from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
|
|
23
34
|
from snowflake.cli.api.entities.utils import (
|
|
35
|
+
drop_generic_object,
|
|
24
36
|
ensure_correct_owner,
|
|
37
|
+
execute_post_deploy_hooks,
|
|
25
38
|
generic_sql_error_handler,
|
|
26
39
|
render_script_templates,
|
|
40
|
+
sync_deploy_root_with_stage,
|
|
41
|
+
validation_item_to_str,
|
|
42
|
+
)
|
|
43
|
+
from snowflake.cli.api.errno import (
|
|
44
|
+
DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
|
|
27
45
|
)
|
|
28
46
|
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
|
|
29
47
|
from snowflake.cli.api.project.schemas.entities.application_package_entity_model import (
|
|
30
48
|
ApplicationPackageEntityModel,
|
|
31
49
|
)
|
|
50
|
+
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
|
|
51
|
+
from snowflake.cli.api.project.util import extract_schema
|
|
32
52
|
from snowflake.cli.api.rendering.jinja import (
|
|
33
|
-
|
|
53
|
+
get_basic_jinja_env,
|
|
34
54
|
)
|
|
35
55
|
from snowflake.connector import ProgrammingError
|
|
56
|
+
from snowflake.connector.cursor import DictCursor
|
|
36
57
|
|
|
37
58
|
|
|
38
59
|
class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
@@ -57,6 +78,89 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
57
78
|
compiler.compile_artifacts()
|
|
58
79
|
return bundle_map
|
|
59
80
|
|
|
81
|
+
def action_deploy(
|
|
82
|
+
self,
|
|
83
|
+
ctx: ActionContext,
|
|
84
|
+
prune: bool,
|
|
85
|
+
recursive: bool,
|
|
86
|
+
paths: List[Path],
|
|
87
|
+
validate: bool,
|
|
88
|
+
):
|
|
89
|
+
model = self._entity_model
|
|
90
|
+
package_name = model.fqn.identifier
|
|
91
|
+
if model.meta and model.meta.role:
|
|
92
|
+
package_role = model.meta.role
|
|
93
|
+
else:
|
|
94
|
+
package_role = ctx.default_role
|
|
95
|
+
|
|
96
|
+
# 1. Create a bundle
|
|
97
|
+
bundle_map = self.action_bundle(ctx)
|
|
98
|
+
|
|
99
|
+
# 2. Create an empty application package, if none exists
|
|
100
|
+
self.create_app_package(
|
|
101
|
+
console=ctx.console,
|
|
102
|
+
package_name=package_name,
|
|
103
|
+
package_role=package_role,
|
|
104
|
+
package_distribution=model.distribution,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
with get_sql_executor().use_role(package_role):
|
|
108
|
+
# 3. Upload files from deploy root local folder to the above stage
|
|
109
|
+
stage_fqn = f"{package_name}.{model.stage}"
|
|
110
|
+
stage_schema = extract_schema(stage_fqn)
|
|
111
|
+
sync_deploy_root_with_stage(
|
|
112
|
+
console=ctx.console,
|
|
113
|
+
deploy_root=Path(model.deploy_root),
|
|
114
|
+
package_name=package_name,
|
|
115
|
+
stage_schema=stage_schema,
|
|
116
|
+
bundle_map=bundle_map,
|
|
117
|
+
role=package_role,
|
|
118
|
+
prune=prune,
|
|
119
|
+
recursive=recursive,
|
|
120
|
+
stage_fqn=stage_fqn,
|
|
121
|
+
local_paths_to_sync=paths,
|
|
122
|
+
print_diff=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
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,
|
|
130
|
+
package_name=package_name,
|
|
131
|
+
package_warehouse=model.meta.warehouse or ctx.default_warehouse,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if validate:
|
|
135
|
+
self.validate_setup_script(
|
|
136
|
+
console=ctx.console,
|
|
137
|
+
package_name=package_name,
|
|
138
|
+
package_role=package_role,
|
|
139
|
+
stage_fqn=stage_fqn,
|
|
140
|
+
use_scratch_stage=False,
|
|
141
|
+
scratch_stage_fqn="",
|
|
142
|
+
deploy_to_scratch_stage_fn=lambda *args: None,
|
|
143
|
+
)
|
|
144
|
+
|
|
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
|
+
)
|
|
163
|
+
|
|
60
164
|
@staticmethod
|
|
61
165
|
def get_existing_app_pkg_info(
|
|
62
166
|
package_name: str,
|
|
@@ -178,9 +282,9 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
178
282
|
|
|
179
283
|
queued_queries = render_script_templates(
|
|
180
284
|
project_root,
|
|
181
|
-
jinja_render_from_str,
|
|
182
285
|
dict(package_name=package_name),
|
|
183
286
|
package_scripts,
|
|
287
|
+
get_basic_jinja_env(),
|
|
184
288
|
)
|
|
185
289
|
|
|
186
290
|
# once we're sure all the templates expanded correctly, execute all of them
|
|
@@ -258,3 +362,192 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
|
|
|
258
362
|
"""
|
|
259
363
|
)
|
|
260
364
|
)
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def execute_post_deploy_hooks(
|
|
368
|
+
cls,
|
|
369
|
+
console: AbstractConsole,
|
|
370
|
+
project_root: Path,
|
|
371
|
+
post_deploy_hooks: Optional[List[PostDeployHook]],
|
|
372
|
+
package_name: str,
|
|
373
|
+
package_warehouse: Optional[str],
|
|
374
|
+
):
|
|
375
|
+
with cls.use_package_warehouse(package_warehouse):
|
|
376
|
+
execute_post_deploy_hooks(
|
|
377
|
+
console=console,
|
|
378
|
+
project_root=project_root,
|
|
379
|
+
post_deploy_hooks=post_deploy_hooks,
|
|
380
|
+
deployed_object_type="application package",
|
|
381
|
+
database_name=package_name,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def validate_setup_script(
|
|
386
|
+
cls,
|
|
387
|
+
console: AbstractConsole,
|
|
388
|
+
package_name: str,
|
|
389
|
+
package_role: str,
|
|
390
|
+
stage_fqn: str,
|
|
391
|
+
use_scratch_stage: bool,
|
|
392
|
+
scratch_stage_fqn: str,
|
|
393
|
+
deploy_to_scratch_stage_fn: Callable,
|
|
394
|
+
):
|
|
395
|
+
"""Validates Native App setup script SQL."""
|
|
396
|
+
with console.phase(f"Validating Snowflake Native App setup script."):
|
|
397
|
+
validation_result = cls.get_validation_result(
|
|
398
|
+
console=console,
|
|
399
|
+
package_name=package_name,
|
|
400
|
+
package_role=package_role,
|
|
401
|
+
stage_fqn=stage_fqn,
|
|
402
|
+
use_scratch_stage=use_scratch_stage,
|
|
403
|
+
scratch_stage_fqn=scratch_stage_fqn,
|
|
404
|
+
deploy_to_scratch_stage_fn=deploy_to_scratch_stage_fn,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# First print warnings, regardless of the outcome of validation
|
|
408
|
+
for warning in validation_result.get("warnings", []):
|
|
409
|
+
console.warning(validation_item_to_str(warning))
|
|
410
|
+
|
|
411
|
+
# Then print errors
|
|
412
|
+
for error in validation_result.get("errors", []):
|
|
413
|
+
# Print them as warnings for now since we're going to be
|
|
414
|
+
# revamping CLI output soon
|
|
415
|
+
console.warning(validation_item_to_str(error))
|
|
416
|
+
|
|
417
|
+
# Then raise an exception if validation failed
|
|
418
|
+
if validation_result["status"] == "FAIL":
|
|
419
|
+
raise SetupScriptFailedValidation()
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def get_validation_result(
|
|
423
|
+
console: AbstractConsole,
|
|
424
|
+
package_name: str,
|
|
425
|
+
package_role: str,
|
|
426
|
+
stage_fqn: str,
|
|
427
|
+
use_scratch_stage: bool,
|
|
428
|
+
scratch_stage_fqn: str,
|
|
429
|
+
deploy_to_scratch_stage_fn: Callable,
|
|
430
|
+
):
|
|
431
|
+
"""Call system$validate_native_app_setup() to validate deployed Native App setup script."""
|
|
432
|
+
if use_scratch_stage:
|
|
433
|
+
stage_fqn = scratch_stage_fqn
|
|
434
|
+
deploy_to_scratch_stage_fn()
|
|
435
|
+
prefixed_stage_fqn = StageManager.get_standard_stage_prefix(stage_fqn)
|
|
436
|
+
sql_executor = get_sql_executor()
|
|
437
|
+
try:
|
|
438
|
+
cursor = sql_executor.execute_query(
|
|
439
|
+
f"call system$validate_native_app_setup('{prefixed_stage_fqn}')"
|
|
440
|
+
)
|
|
441
|
+
except ProgrammingError as err:
|
|
442
|
+
if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
|
|
443
|
+
raise ApplicationPackageDoesNotExistError(package_name)
|
|
444
|
+
generic_sql_error_handler(err)
|
|
445
|
+
else:
|
|
446
|
+
if not cursor.rowcount:
|
|
447
|
+
raise SnowflakeSQLExecutionError()
|
|
448
|
+
return json.loads(cursor.fetchone()[0])
|
|
449
|
+
finally:
|
|
450
|
+
if use_scratch_stage:
|
|
451
|
+
console.step(f"Dropping stage {scratch_stage_fqn}.")
|
|
452
|
+
with sql_executor.use_role(package_role):
|
|
453
|
+
sql_executor.execute_query(
|
|
454
|
+
f"drop stage if exists {scratch_stage_fqn}"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def drop(
|
|
459
|
+
cls,
|
|
460
|
+
console: AbstractConsole,
|
|
461
|
+
package_name: str,
|
|
462
|
+
package_role: str,
|
|
463
|
+
force_drop: bool,
|
|
464
|
+
):
|
|
465
|
+
sql_executor = get_sql_executor()
|
|
466
|
+
needs_confirm = True
|
|
467
|
+
|
|
468
|
+
# 1. If existing application package is not found, exit gracefully
|
|
469
|
+
show_obj_row = cls.get_existing_app_pkg_info(
|
|
470
|
+
package_name=package_name,
|
|
471
|
+
package_role=package_role,
|
|
472
|
+
)
|
|
473
|
+
if show_obj_row is None:
|
|
474
|
+
console.warning(
|
|
475
|
+
f"Role {package_role} does not own any application package with the name {package_name}, or the application package does not exist."
|
|
476
|
+
)
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# 2. Check for the right owner
|
|
480
|
+
ensure_correct_owner(row=show_obj_row, role=package_role, obj_name=package_name)
|
|
481
|
+
|
|
482
|
+
with sql_executor.use_role(package_role):
|
|
483
|
+
# 3. Check for versions in the application package
|
|
484
|
+
show_versions_query = f"show versions in application package {package_name}"
|
|
485
|
+
show_versions_cursor = sql_executor.execute_query(
|
|
486
|
+
show_versions_query, cursor_class=DictCursor
|
|
487
|
+
)
|
|
488
|
+
if show_versions_cursor.rowcount is None:
|
|
489
|
+
raise SnowflakeSQLExecutionError(show_versions_query)
|
|
490
|
+
|
|
491
|
+
if show_versions_cursor.rowcount > 0:
|
|
492
|
+
# allow dropping a package with versions when --force is set
|
|
493
|
+
if not force_drop:
|
|
494
|
+
raise CouldNotDropApplicationPackageWithVersions(
|
|
495
|
+
"Drop versions first, or use --force to override."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# 4. Check distribution of the existing application package
|
|
499
|
+
actual_distribution = cls.get_app_pkg_distribution_in_snowflake(
|
|
500
|
+
package_name=package_name,
|
|
501
|
+
package_role=package_role,
|
|
502
|
+
)
|
|
503
|
+
if not cls.verify_project_distribution(
|
|
504
|
+
console=console,
|
|
505
|
+
package_name=package_name,
|
|
506
|
+
package_role=package_role,
|
|
507
|
+
package_distribution=actual_distribution,
|
|
508
|
+
):
|
|
509
|
+
console.warning(
|
|
510
|
+
f"Dropping application package {package_name} with distribution '{actual_distribution}'."
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# 5. If distribution is internal, check if created by the Snowflake CLI
|
|
514
|
+
row_comment = show_obj_row[COMMENT_COL]
|
|
515
|
+
if actual_distribution == INTERNAL_DISTRIBUTION:
|
|
516
|
+
if row_comment in ALLOWED_SPECIAL_COMMENTS:
|
|
517
|
+
needs_confirm = False
|
|
518
|
+
else:
|
|
519
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
520
|
+
console.warning(
|
|
521
|
+
f"Application package {package_name} was not created by Snowflake CLI."
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
525
|
+
console.warning(
|
|
526
|
+
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."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if needs_confirmation(needs_confirm, force_drop):
|
|
530
|
+
should_drop_object = typer.confirm(
|
|
531
|
+
dedent(
|
|
532
|
+
f"""\
|
|
533
|
+
Application package details:
|
|
534
|
+
Name: {package_name}
|
|
535
|
+
Created on: {show_obj_row["created_on"]}
|
|
536
|
+
Distribution: {actual_distribution}
|
|
537
|
+
Owner: {show_obj_row[OWNER_COL]}
|
|
538
|
+
Comment: {show_obj_row[COMMENT_COL]}
|
|
539
|
+
Are you sure you want to drop it?
|
|
540
|
+
"""
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
if not should_drop_object:
|
|
544
|
+
console.message(f"Did not drop application package {package_name}.")
|
|
545
|
+
return # The user desires to keep the application package, therefore exit gracefully
|
|
546
|
+
|
|
547
|
+
# All validations have passed, drop object
|
|
548
|
+
drop_generic_object(
|
|
549
|
+
console=console,
|
|
550
|
+
object_type="application package",
|
|
551
|
+
object_name=package_name,
|
|
552
|
+
role=package_role,
|
|
553
|
+
)
|
|
@@ -7,6 +7,8 @@ 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"
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
T = TypeVar("T")
|
|
@@ -35,11 +37,13 @@ class EntityBase(Generic[T]):
|
|
|
35
37
|
"""
|
|
36
38
|
return callable(getattr(self, action, None))
|
|
37
39
|
|
|
38
|
-
def perform(
|
|
40
|
+
def perform(
|
|
41
|
+
self, action: EntityActions, action_ctx: ActionContext, *args, **kwargs
|
|
42
|
+
):
|
|
39
43
|
"""
|
|
40
44
|
Performs the requested action.
|
|
41
45
|
"""
|
|
42
|
-
return getattr(self, action)(action_ctx)
|
|
46
|
+
return getattr(self, action)(action_ctx, *args, **kwargs)
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
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,16 +25,18 @@ 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
|
|
@@ -272,8 +274,7 @@ def execute_post_deploy_hooks(
|
|
|
272
274
|
|
|
273
275
|
scripts_content_list = render_script_templates(
|
|
274
276
|
project_root,
|
|
275
|
-
|
|
276
|
-
{},
|
|
277
|
+
get_cli_context().template_context,
|
|
277
278
|
sql_scripts_paths,
|
|
278
279
|
)
|
|
279
280
|
|
|
@@ -287,16 +288,17 @@ def execute_post_deploy_hooks(
|
|
|
287
288
|
|
|
288
289
|
def render_script_templates(
|
|
289
290
|
project_root: Path,
|
|
290
|
-
render_from_str: Callable[[str, Dict[str, Any]], str],
|
|
291
291
|
jinja_context: dict[str, Any],
|
|
292
292
|
scripts: List[str],
|
|
293
|
+
override_env: Optional[jinja2.Environment] = None,
|
|
293
294
|
) -> List[str]:
|
|
294
295
|
"""
|
|
295
296
|
Input:
|
|
296
297
|
- project_root: path to project root
|
|
297
|
-
- render_from_str: function which renders a jinja template from a string and jinja context
|
|
298
298
|
- jinja_context: a dictionary with the jinja context
|
|
299
299
|
- scripts: list of script paths relative to the project root
|
|
300
|
+
- override_env: optional jinja environment to use for rendering,
|
|
301
|
+
if not provided, the environment will be chosen based on the template syntax
|
|
300
302
|
Returns:
|
|
301
303
|
- List of rendered scripts content
|
|
302
304
|
Size of the return list is the same as the size of the input scripts list.
|
|
@@ -306,16 +308,50 @@ def render_script_templates(
|
|
|
306
308
|
script_full_path = SecurePath(project_root) / relpath
|
|
307
309
|
try:
|
|
308
310
|
template_content = script_full_path.read_text(file_size_limit_mb=UNLIMITED)
|
|
309
|
-
|
|
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)
|
|
310
315
|
scripts_contents.append(result)
|
|
311
316
|
|
|
312
317
|
except FileNotFoundError as e:
|
|
313
318
|
raise MissingScriptError(relpath) from e
|
|
314
319
|
|
|
315
320
|
except jinja2.TemplateSyntaxError as e:
|
|
316
|
-
raise
|
|
321
|
+
raise InvalidTemplateInFileError(relpath, e, e.lineno) from e
|
|
317
322
|
|
|
318
323
|
except jinja2.UndefinedError as e:
|
|
319
|
-
raise
|
|
324
|
+
raise InvalidTemplateInFileError(relpath, e) from e
|
|
320
325
|
|
|
321
326
|
return scripts_contents
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def validation_item_to_str(item: dict[str, str | int]):
|
|
330
|
+
s = item["message"]
|
|
331
|
+
if item["errorCode"]:
|
|
332
|
+
s = f"{s} (error code {item['errorCode']})"
|
|
333
|
+
return s
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def drop_generic_object(
|
|
337
|
+
console: AbstractConsole,
|
|
338
|
+
object_type: str,
|
|
339
|
+
object_name: str,
|
|
340
|
+
role: str,
|
|
341
|
+
cascade: bool = False,
|
|
342
|
+
):
|
|
343
|
+
"""
|
|
344
|
+
Drop object using the given role.
|
|
345
|
+
"""
|
|
346
|
+
sql_executor = get_sql_executor()
|
|
347
|
+
with sql_executor.use_role(role):
|
|
348
|
+
console.step(f"Dropping {object_type} {object_name} now.")
|
|
349
|
+
drop_query = f"drop {object_type} {object_name}"
|
|
350
|
+
if cascade:
|
|
351
|
+
drop_query += " cascade"
|
|
352
|
+
try:
|
|
353
|
+
sql_executor.execute_query(drop_query)
|
|
354
|
+
except:
|
|
355
|
+
raise SnowflakeSQLExecutionError(drop_query)
|
|
356
|
+
|
|
357
|
+
console.message(f"Dropped {object_type} {object_name} successfully.")
|
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):
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import List, Optional
|
|
19
19
|
|
|
20
20
|
import yaml
|
|
21
|
+
from click import ClickException
|
|
21
22
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
22
23
|
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
|
|
23
24
|
from snowflake.cli.api.project.schemas.project_definition import (
|
|
@@ -45,8 +46,13 @@ def _get_merged_definitions(paths: List[Path]) -> Optional[Definition]:
|
|
|
45
46
|
if len(spaths) == 0:
|
|
46
47
|
return None
|
|
47
48
|
|
|
49
|
+
loader = yaml.BaseLoader
|
|
50
|
+
loader.add_constructor(
|
|
51
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_duplicates_constructor
|
|
52
|
+
)
|
|
53
|
+
|
|
48
54
|
with spaths[0].open("r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as base_yml:
|
|
49
|
-
definition = yaml.load(base_yml.read(), Loader=
|
|
55
|
+
definition = yaml.load(base_yml.read(), Loader=loader) or {}
|
|
50
56
|
|
|
51
57
|
for override_path in spaths[1:]:
|
|
52
58
|
with override_path.open(
|
|
@@ -90,3 +96,20 @@ def default_role():
|
|
|
90
96
|
def default_application(project_name: str):
|
|
91
97
|
user = sanitize_identifier(get_env_username() or DEFAULT_USERNAME).lower()
|
|
92
98
|
return append_to_identifier(to_identifier(project_name), f"_{user}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _no_duplicates_constructor(loader, node, deep=False):
|
|
102
|
+
"""
|
|
103
|
+
Raises error it there are duplicated keys on the same level in the yaml file
|
|
104
|
+
"""
|
|
105
|
+
mapping = {}
|
|
106
|
+
|
|
107
|
+
for key_node, value_node in node.value:
|
|
108
|
+
key = loader.construct_object(key_node, deep=deep)
|
|
109
|
+
value = loader.construct_object(value_node, deep=deep)
|
|
110
|
+
if key in mapping.keys():
|
|
111
|
+
raise ClickException(
|
|
112
|
+
f"While loading the project definition file, duplicate key was found: {key}"
|
|
113
|
+
)
|
|
114
|
+
mapping[key] = value
|
|
115
|
+
return loader.construct_mapping(node, deep)
|