snowflake-cli 3.0.2__py3-none-any.whl → 3.2.0__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 +3 -0
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
- snowflake/cli/_app/telemetry.py +69 -4
- snowflake/cli/_plugins/connection/commands.py +152 -99
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +9 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +144 -188
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
- snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +63 -21
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
- snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
- snowflake/cli/_plugins/spcs/services/commands.py +100 -17
- snowflake/cli/_plugins/spcs/services/manager.py +108 -16
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +28 -19
- snowflake/cli/_plugins/stage/diff.py +17 -17
- snowflake/cli/_plugins/stage/manager.py +304 -84
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +27 -4
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +4 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +30 -9
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +36 -69
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +41 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +293 -77
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +40 -29
- snowflake/cli/api/stage_path.py +244 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
+
from io import StringIO
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from textwrap import dedent
|
|
20
21
|
from typing import List, Optional
|
|
21
22
|
|
|
22
23
|
import typer
|
|
23
|
-
|
|
24
|
+
import yaml
|
|
24
25
|
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
25
26
|
from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
26
27
|
ForceOption,
|
|
@@ -43,6 +44,24 @@ ws = SnowTyperFactory(
|
|
|
43
44
|
log = logging.getLogger(__name__)
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
@ws.command(requires_connection=False, hidden=True)
|
|
48
|
+
@with_project_definition()
|
|
49
|
+
def dump(**options):
|
|
50
|
+
"""
|
|
51
|
+
Dumps the project definition.
|
|
52
|
+
"""
|
|
53
|
+
cli_context = get_cli_context()
|
|
54
|
+
pd = cli_context.project_definition
|
|
55
|
+
io = StringIO()
|
|
56
|
+
yaml.safe_dump(
|
|
57
|
+
pd.model_dump(mode="json", by_alias=True),
|
|
58
|
+
io,
|
|
59
|
+
sort_keys=False,
|
|
60
|
+
width=float("inf"), # Don't break lines
|
|
61
|
+
)
|
|
62
|
+
return MessageResult(io.getvalue())
|
|
63
|
+
|
|
64
|
+
|
|
46
65
|
@ws.command(requires_connection=True, hidden=True)
|
|
47
66
|
@with_project_definition()
|
|
48
67
|
def bundle(
|
|
@@ -243,6 +262,11 @@ def version_create(
|
|
|
243
262
|
help=f"""The patch number you want to create for an existing version.
|
|
244
263
|
Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""",
|
|
245
264
|
),
|
|
265
|
+
label: Optional[str] = typer.Option(
|
|
266
|
+
None,
|
|
267
|
+
"--label",
|
|
268
|
+
help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
|
|
269
|
+
),
|
|
246
270
|
skip_git_check: Optional[bool] = typer.Option(
|
|
247
271
|
False,
|
|
248
272
|
"--skip-git-check",
|
|
@@ -254,8 +278,6 @@ def version_create(
|
|
|
254
278
|
**options,
|
|
255
279
|
):
|
|
256
280
|
"""Creates a new version for the specified entity."""
|
|
257
|
-
if version is None and patch is not None:
|
|
258
|
-
raise MissingParameter("Cannot provide a patch without version!")
|
|
259
281
|
|
|
260
282
|
cli_context = get_cli_context()
|
|
261
283
|
ws = WorkspaceManager(
|
|
@@ -267,6 +289,7 @@ def version_create(
|
|
|
267
289
|
EntityActions.VERSION_CREATE,
|
|
268
290
|
version=version,
|
|
269
291
|
patch=patch,
|
|
292
|
+
label=label,
|
|
270
293
|
skip_git_check=skip_git_check,
|
|
271
294
|
interactive=interactive,
|
|
272
295
|
force=force,
|
|
@@ -299,7 +322,7 @@ def version_drop(
|
|
|
299
322
|
)
|
|
300
323
|
ws.perform_action(
|
|
301
324
|
entity_id,
|
|
302
|
-
EntityActions.
|
|
325
|
+
EntityActions.VERSION_DROP,
|
|
303
326
|
version=version,
|
|
304
327
|
interactive=interactive,
|
|
305
328
|
force=force,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from snowflake.cli.api.console.abc import AbstractConsole
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class WorkspaceContext:
|
|
11
|
+
"""
|
|
12
|
+
An object that is passed to each entity when instantiated by WorkspaceManager
|
|
13
|
+
to allow access to the CLI context without requiring the entities to use
|
|
14
|
+
get_cli_context().
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
console: AbstractConsole
|
|
18
|
+
project_root: Path
|
|
19
|
+
get_default_role: Callable[[], str]
|
|
20
|
+
get_default_warehouse: Callable[[], str | None]
|
|
21
|
+
|
|
22
|
+
@cached_property
|
|
23
|
+
def default_role(self) -> str:
|
|
24
|
+
return self.get_default_role()
|
|
25
|
+
|
|
26
|
+
@cached_property
|
|
27
|
+
def default_warehouse(self) -> str | None:
|
|
28
|
+
return self.get_default_warehouse()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ActionContext:
|
|
33
|
+
"""
|
|
34
|
+
An object that is passed to each action when called by WorkspaceManager
|
|
35
|
+
to provide access to metadata about the entity and project being acted upon.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
get_entity: Callable
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Dict
|
|
3
3
|
|
|
4
|
-
from snowflake.cli._plugins.workspace.
|
|
4
|
+
from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
|
|
5
5
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
6
6
|
from snowflake.cli.api.console import cli_console as cc
|
|
7
7
|
from snowflake.cli.api.entities.common import EntityActions, get_sql_executor
|
|
@@ -31,13 +31,6 @@ class WorkspaceManager:
|
|
|
31
31
|
self._entities_cache: Dict[str, Entity] = {}
|
|
32
32
|
self._project_definition: DefinitionV20 = project_definition
|
|
33
33
|
self._project_root = project_root
|
|
34
|
-
self._default_role = default_role()
|
|
35
|
-
if self._default_role is None:
|
|
36
|
-
self._default_role = get_sql_executor().current_role()
|
|
37
|
-
self.default_warehouse = None
|
|
38
|
-
cli_context = get_cli_context()
|
|
39
|
-
if cli_context.connection.warehouse:
|
|
40
|
-
self.default_warehouse = to_identifier(cli_context.connection.warehouse)
|
|
41
34
|
|
|
42
35
|
def get_entity(self, entity_id: str):
|
|
43
36
|
"""
|
|
@@ -50,7 +43,13 @@ class WorkspaceManager:
|
|
|
50
43
|
raise ValueError(f"No such entity ID: {entity_id}")
|
|
51
44
|
entity_model_cls = entity_model.__class__
|
|
52
45
|
entity_cls = v2_entity_model_to_entity_map[entity_model_cls]
|
|
53
|
-
|
|
46
|
+
workspace_ctx = WorkspaceContext(
|
|
47
|
+
console=cc,
|
|
48
|
+
project_root=self.project_root,
|
|
49
|
+
get_default_role=_get_default_role,
|
|
50
|
+
get_default_warehouse=_get_default_warehouse,
|
|
51
|
+
)
|
|
52
|
+
self._entities_cache[entity_id] = entity_cls(entity_model, workspace_ctx)
|
|
54
53
|
return self._entities_cache[entity_id]
|
|
55
54
|
|
|
56
55
|
def perform_action(self, entity_id: str, action: EntityActions, *args, **kwargs):
|
|
@@ -60,15 +59,26 @@ class WorkspaceManager:
|
|
|
60
59
|
entity = self.get_entity(entity_id)
|
|
61
60
|
if entity.supports(action):
|
|
62
61
|
action_ctx = ActionContext(
|
|
63
|
-
console=cc,
|
|
64
|
-
project_root=self.project_root(),
|
|
65
|
-
default_role=self._default_role,
|
|
66
|
-
default_warehouse=self.default_warehouse,
|
|
67
62
|
get_entity=self.get_entity,
|
|
68
63
|
)
|
|
69
64
|
return entity.perform(action, action_ctx, *args, **kwargs)
|
|
70
65
|
else:
|
|
71
66
|
raise ValueError(f'This entity type does not support "{action.value}"')
|
|
72
67
|
|
|
68
|
+
@property
|
|
73
69
|
def project_root(self) -> Path:
|
|
74
70
|
return self._project_root
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_default_role() -> str:
|
|
74
|
+
role = default_role()
|
|
75
|
+
if role is None:
|
|
76
|
+
role = get_sql_executor().current_role()
|
|
77
|
+
return role
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_default_warehouse() -> str | None:
|
|
81
|
+
warehouse = get_cli_context().connection.warehouse
|
|
82
|
+
if warehouse:
|
|
83
|
+
warehouse = to_identifier(warehouse)
|
|
84
|
+
return warehouse
|
|
@@ -54,9 +54,9 @@ class _CliGlobalContextManager:
|
|
|
54
54
|
project_env_overrides_args: dict[str, str] = field(default_factory=dict)
|
|
55
55
|
|
|
56
56
|
# FIXME: this property only exists to help implement
|
|
57
|
-
# nativeapp_definition_v2_to_v1
|
|
58
|
-
# this calculation is provided to commands
|
|
59
|
-
# this logic (then make project_definition a non-cloned @property)
|
|
57
|
+
# nativeapp_definition_v2_to_v1 and single_app_and_package.
|
|
58
|
+
# Consider changing the way this calculation is provided to commands
|
|
59
|
+
# in order to remove this logic (then make project_definition a non-cloned @property)
|
|
60
60
|
override_project_definition: ProjectDefinition | None = None
|
|
61
61
|
|
|
62
62
|
_definition_manager: DefinitionManager | None = None
|
|
@@ -76,6 +76,7 @@ class _CliGlobalContextManager:
|
|
|
76
76
|
self,
|
|
77
77
|
connection_context=self.connection_context.clone(),
|
|
78
78
|
project_env_overrides_args=self.project_env_overrides_args.copy(),
|
|
79
|
+
metrics=self.metrics.clone(),
|
|
79
80
|
)
|
|
80
81
|
|
|
81
82
|
def __setattr__(self, prop, val):
|
|
@@ -33,6 +33,7 @@ from snowflake.cli.api.connections import ConnectionContext
|
|
|
33
33
|
from snowflake.cli.api.console import cli_console
|
|
34
34
|
from snowflake.cli.api.identifiers import FQN
|
|
35
35
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
36
|
+
from snowflake.cli.api.stage_path import StagePath
|
|
36
37
|
|
|
37
38
|
DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
|
|
38
39
|
|
|
@@ -83,7 +84,7 @@ ConnectionOption = typer.Option(
|
|
|
83
84
|
"--connection",
|
|
84
85
|
"-c",
|
|
85
86
|
"--environment",
|
|
86
|
-
help=f"Name of the connection, as defined in your `config.toml
|
|
87
|
+
help=f"Name of the connection, as defined in your `config.toml` file. Default: `default`.",
|
|
87
88
|
callback=_connection_callback("connection_name"),
|
|
88
89
|
show_default=False,
|
|
89
90
|
rich_help_panel=_CONNECTION_SECTION,
|
|
@@ -276,7 +277,7 @@ MfaPasscodeOption = typer.Option(
|
|
|
276
277
|
EnableDiagOption = typer.Option(
|
|
277
278
|
False,
|
|
278
279
|
"--enable-diag",
|
|
279
|
-
help="Run
|
|
280
|
+
help="Run Python connector diagnostic test",
|
|
280
281
|
callback=_connection_callback("enable_diag"),
|
|
281
282
|
show_default=False,
|
|
282
283
|
is_flag=True,
|
|
@@ -349,7 +350,7 @@ VerboseOption = typer.Option(
|
|
|
349
350
|
DebugOption = typer.Option(
|
|
350
351
|
False,
|
|
351
352
|
"--debug",
|
|
352
|
-
help="Displays log entries for log levels `debug` and higher; debug logs
|
|
353
|
+
help="Displays log entries for log levels `debug` and higher; debug logs contain additional information.",
|
|
353
354
|
callback=_context_callback("enable_tracebacks"),
|
|
354
355
|
is_flag=True,
|
|
355
356
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
@@ -406,9 +407,9 @@ def variables_option(description: str):
|
|
|
406
407
|
|
|
407
408
|
|
|
408
409
|
ExecuteVariablesOption = variables_option(
|
|
409
|
-
'Variables for the execution context
|
|
410
|
-
"For SQL files variables are
|
|
411
|
-
"For Python files variables are used to update os.environ dictionary. Provided keys are capitalized to adhere to best practices."
|
|
410
|
+
'Variables for the execution context; for example: `-D "<key>=<value>"`. '
|
|
411
|
+
"For SQL files, variables are used to expand the template, and any unknown variable will cause an error (consider embedding quoting in the file)."
|
|
412
|
+
"For Python files, variables are used to update the os.environ dictionary. Provided keys are capitalized to adhere to best practices. "
|
|
412
413
|
"In case of SQL files string values must be quoted in `''` (consider embedding quoting in the file).",
|
|
413
414
|
)
|
|
414
415
|
|
|
@@ -473,6 +474,13 @@ class IdentifierStageType(click.ParamType):
|
|
|
473
474
|
return FQN.from_stage(value)
|
|
474
475
|
|
|
475
476
|
|
|
477
|
+
class IdentifierStagePathType(click.ParamType):
|
|
478
|
+
name = "TEXT"
|
|
479
|
+
|
|
480
|
+
def convert(self, value, param, ctx):
|
|
481
|
+
return StagePath.from_stage_str(value)
|
|
482
|
+
|
|
483
|
+
|
|
476
484
|
def identifier_argument(
|
|
477
485
|
sf_object: str,
|
|
478
486
|
example: str,
|
|
@@ -481,7 +489,7 @@ def identifier_argument(
|
|
|
481
489
|
) -> typer.Argument:
|
|
482
490
|
return typer.Argument(
|
|
483
491
|
...,
|
|
484
|
-
help=f"Identifier of the {sf_object}
|
|
492
|
+
help=f"Identifier of the {sf_object}; for example: {example}",
|
|
485
493
|
show_default=False,
|
|
486
494
|
click_type=click_type,
|
|
487
495
|
callback=callback,
|
|
@@ -496,6 +504,14 @@ def identifier_stage_argument(
|
|
|
496
504
|
)
|
|
497
505
|
|
|
498
506
|
|
|
507
|
+
def identifier_stage_path_argument(
|
|
508
|
+
sf_object: str, example: str, callback: Callable | None = None
|
|
509
|
+
) -> typer.Argument:
|
|
510
|
+
return identifier_argument(
|
|
511
|
+
sf_object, example, click_type=IdentifierStagePathType(), callback=callback
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
499
515
|
def execution_identifier_argument(sf_object: str, example: str) -> typer.Argument:
|
|
500
516
|
return typer.Argument(
|
|
501
517
|
...,
|
snowflake/cli/api/config.py
CHANGED
|
@@ -181,7 +181,7 @@ def _read_config_file():
|
|
|
181
181
|
)
|
|
182
182
|
warnings.warn(
|
|
183
183
|
f"Unauthorized users ({users}) have access to configuration file {CONFIG_MANAGER.file_path}.\n"
|
|
184
|
-
f'Run `icacls "{CONFIG_MANAGER.file_path}" /
|
|
184
|
+
f'Run `icacls "{CONFIG_MANAGER.file_path}" /remove:g <USER_ID>` on those users to restrict permissions.'
|
|
185
185
|
)
|
|
186
186
|
|
|
187
187
|
try:
|
|
@@ -340,16 +340,34 @@ def _get_envs_for_path(*path) -> dict:
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
|
|
343
|
-
def _dump_config(
|
|
343
|
+
def _dump_config(config_and_connections: Dict):
|
|
344
|
+
config_toml_dict = config_and_connections.copy()
|
|
345
|
+
|
|
346
|
+
if CONNECTIONS_FILE.exists():
|
|
347
|
+
# update connections in connections.toml
|
|
348
|
+
# it will add only connections (maybe updated) which were originally read from connections.toml
|
|
349
|
+
# it won't add connections from config.toml
|
|
350
|
+
# because config manager doesn't have connections from config.toml if connections.toml exists
|
|
351
|
+
_update_connections_toml(config_and_connections.get("connections") or {})
|
|
352
|
+
# to config.toml save only connections from config.toml
|
|
353
|
+
connections_to_save_in_config_toml = _read_config_file_toml().get("connections")
|
|
354
|
+
if connections_to_save_in_config_toml:
|
|
355
|
+
config_toml_dict["connections"] = connections_to_save_in_config_toml
|
|
356
|
+
else:
|
|
357
|
+
config_toml_dict.pop("connections", None)
|
|
358
|
+
|
|
344
359
|
with SecurePath(CONFIG_MANAGER.file_path).open("w+") as fh:
|
|
345
|
-
dump(
|
|
360
|
+
dump(config_toml_dict, fh)
|
|
346
361
|
|
|
347
362
|
|
|
348
363
|
def _check_default_config_files_permissions() -> None:
|
|
349
|
-
if
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
364
|
+
if not IS_WINDOWS:
|
|
365
|
+
if CONNECTIONS_FILE.exists() and not file_permissions_are_strict(
|
|
366
|
+
CONNECTIONS_FILE
|
|
367
|
+
):
|
|
368
|
+
raise ConfigFileTooWidePermissionsError(CONNECTIONS_FILE)
|
|
369
|
+
if CONFIG_FILE.exists() and not file_permissions_are_strict(CONFIG_FILE):
|
|
370
|
+
raise ConfigFileTooWidePermissionsError(CONFIG_FILE)
|
|
353
371
|
|
|
354
372
|
|
|
355
373
|
from typing import Literal
|
|
@@ -370,9 +388,12 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
|
|
|
370
388
|
return {k: _bool_or_unknown(v) for k, v in flags.items()}
|
|
371
389
|
|
|
372
390
|
|
|
391
|
+
def _read_config_file_toml() -> dict:
|
|
392
|
+
return tomlkit.loads(CONFIG_MANAGER.file_path.read_text()).unwrap()
|
|
393
|
+
|
|
394
|
+
|
|
373
395
|
def _read_connections_toml() -> dict:
|
|
374
|
-
|
|
375
|
-
return tomlkit.loads(f.read()).unwrap()
|
|
396
|
+
return tomlkit.loads(CONNECTIONS_FILE.read_text()).unwrap()
|
|
376
397
|
|
|
377
398
|
|
|
378
399
|
def _update_connections_toml(connections: dict):
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -22,7 +22,7 @@ from dataclasses import asdict, dataclass, field, fields, replace
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Optional
|
|
24
24
|
|
|
25
|
-
from snowflake.cli.api.config import get_default_connection_name
|
|
25
|
+
from snowflake.cli.api.config import get_connection_dict, get_default_connection_name
|
|
26
26
|
from snowflake.cli.api.exceptions import InvalidSchemaError
|
|
27
27
|
from snowflake.connector import SnowflakeConnection
|
|
28
28
|
from snowflake.connector.compat import IS_WINDOWS
|
|
@@ -79,6 +79,17 @@ class ConnectionContext:
|
|
|
79
79
|
raise KeyError(f"{key} is not a field of {self.__class__.__name__}")
|
|
80
80
|
setattr(self, key, value)
|
|
81
81
|
|
|
82
|
+
def update_from_config(self) -> ConnectionContext:
|
|
83
|
+
connection_config = get_connection_dict(connection_name=self.connection_name)
|
|
84
|
+
if "private_key_path" in connection_config:
|
|
85
|
+
connection_config["private_key_file"] = connection_config[
|
|
86
|
+
"private_key_path"
|
|
87
|
+
]
|
|
88
|
+
del connection_config["private_key_path"]
|
|
89
|
+
|
|
90
|
+
self.update(**connection_config)
|
|
91
|
+
return self
|
|
92
|
+
|
|
82
93
|
def __repr__(self) -> str:
|
|
83
94
|
"""Minimal repr where None values have their keys omitted."""
|
|
84
95
|
items = [f"{k}={repr(v)}" for (k, v) in self.present_values_as_dict().items()]
|
|
@@ -29,10 +29,6 @@ IMPORTANT_STYLE: Style = Style(bold=True, italic=True)
|
|
|
29
29
|
INDENTATION_LEVEL: int = 2
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class CliConsoleNestingProhibitedError(RuntimeError):
|
|
33
|
-
"""CliConsole phase nesting not allowed."""
|
|
34
|
-
|
|
35
|
-
|
|
36
32
|
class CliConsole(AbstractConsole):
|
|
37
33
|
"""An utility for displaying intermediate output.
|
|
38
34
|
|
|
@@ -70,28 +66,21 @@ class CliConsole(AbstractConsole):
|
|
|
70
66
|
@contextmanager
|
|
71
67
|
def phase(self, enter_message: str, exit_message: Optional[str] = None):
|
|
72
68
|
"""A context manager for organising steps into logical group."""
|
|
73
|
-
if self.in_phase:
|
|
74
|
-
raise CliConsoleNestingProhibitedError("Only one phase allowed at a time.")
|
|
75
|
-
if self._extra_indent > 0:
|
|
76
|
-
raise CliConsoleNestingProhibitedError(
|
|
77
|
-
"Phase cannot be used in an indented block."
|
|
78
|
-
)
|
|
79
|
-
|
|
80
69
|
self._print(self._format_message(enter_message, Output.PHASE))
|
|
81
|
-
self.
|
|
70
|
+
self._extra_indent += 1
|
|
82
71
|
|
|
83
72
|
try:
|
|
84
73
|
yield self.step
|
|
85
74
|
finally:
|
|
86
|
-
self.
|
|
75
|
+
self._extra_indent -= 1
|
|
87
76
|
if exit_message:
|
|
88
77
|
self._print(self._format_message(exit_message, Output.PHASE))
|
|
89
78
|
|
|
90
79
|
@contextmanager
|
|
91
80
|
def indented(self):
|
|
92
81
|
"""
|
|
93
|
-
A context manager for temporarily indenting messages and warnings.
|
|
94
|
-
|
|
82
|
+
A context manager for temporarily indenting messages and warnings.
|
|
83
|
+
Multiple indented blocks can be nested (use sparingly).
|
|
95
84
|
"""
|
|
96
85
|
self._extra_indent += 1
|
|
97
86
|
try:
|
|
@@ -104,10 +93,6 @@ class CliConsole(AbstractConsole):
|
|
|
104
93
|
|
|
105
94
|
If called within a phase, the output will be indented.
|
|
106
95
|
"""
|
|
107
|
-
if self._extra_indent > 0:
|
|
108
|
-
raise CliConsoleNestingProhibitedError(
|
|
109
|
-
"Step cannot be used in an indented block."
|
|
110
|
-
)
|
|
111
96
|
text = self._format_message(message, Output.STEP)
|
|
112
97
|
self._print(text)
|
|
113
98
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Generic, Type, TypeVar, get_args
|
|
3
3
|
|
|
4
|
-
from snowflake.cli._plugins.workspace.
|
|
4
|
+
from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext
|
|
5
5
|
from snowflake.cli.api.sql_execution import SqlExecutor
|
|
6
6
|
|
|
7
7
|
|
|
@@ -10,6 +10,7 @@ class EntityActions(str, Enum):
|
|
|
10
10
|
DEPLOY = "action_deploy"
|
|
11
11
|
DROP = "action_drop"
|
|
12
12
|
VALIDATE = "action_validate"
|
|
13
|
+
EVENTS = "action_events"
|
|
13
14
|
|
|
14
15
|
VERSION_LIST = "action_version_list"
|
|
15
16
|
VERSION_CREATE = "action_version_create"
|
|
@@ -24,8 +25,9 @@ class EntityBase(Generic[T]):
|
|
|
24
25
|
Base class for the fully-featured entity classes.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
def __init__(self, entity_model: T):
|
|
28
|
+
def __init__(self, entity_model: T, workspace_ctx: WorkspaceContext):
|
|
28
29
|
self._entity_model = entity_model
|
|
30
|
+
self._workspace_ctx = workspace_ctx
|
|
29
31
|
|
|
30
32
|
@classmethod
|
|
31
33
|
def get_entity_model_type(cls) -> Type[T]:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from textwrap import dedent
|
|
4
3
|
from typing import Any, List, NoReturn, Optional
|
|
5
4
|
|
|
6
5
|
import jinja2
|
|
@@ -13,10 +12,11 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
|
13
12
|
InvalidTemplateInFileError,
|
|
14
13
|
MissingScriptError,
|
|
15
14
|
)
|
|
15
|
+
from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
|
|
16
16
|
from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
|
|
17
17
|
from snowflake.cli._plugins.stage.diff import (
|
|
18
18
|
DiffResult,
|
|
19
|
-
|
|
19
|
+
StagePathType,
|
|
20
20
|
compute_stage_diff,
|
|
21
21
|
preserve_from_diff,
|
|
22
22
|
sync_local_diff_with_stage,
|
|
@@ -30,7 +30,11 @@ from snowflake.cli.api.errno import (
|
|
|
30
30
|
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
|
|
31
31
|
NO_WAREHOUSE_SELECTED_IN_SESSION,
|
|
32
32
|
)
|
|
33
|
-
from snowflake.cli.api.exceptions import
|
|
33
|
+
from snowflake.cli.api.exceptions import (
|
|
34
|
+
DoesNotExistOrUnauthorizedError,
|
|
35
|
+
NoWarehouseSelectedInSessionError,
|
|
36
|
+
SnowflakeSQLExecutionError,
|
|
37
|
+
)
|
|
34
38
|
from snowflake.cli.api.metrics import CLICounterField
|
|
35
39
|
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
|
|
36
40
|
from snowflake.cli.api.rendering.sql_templates import (
|
|
@@ -41,46 +45,21 @@ from snowflake.connector import ProgrammingError
|
|
|
41
45
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
42
46
|
|
|
43
47
|
|
|
44
|
-
def generic_sql_error_handler(
|
|
45
|
-
err: ProgrammingError, role: Optional[str] = None, warehouse: Optional[str] = None
|
|
46
|
-
) -> NoReturn:
|
|
48
|
+
def generic_sql_error_handler(err: ProgrammingError) -> NoReturn:
|
|
47
49
|
# Potential refactor: If moving away from Python 3.8 and 3.9 to >= 3.10, use match ... case
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
'{role}' may not have access to warehouse '{warehouse}'.
|
|
54
|
-
Please grant usage privilege on warehouse to this role.
|
|
55
|
-
"""
|
|
56
|
-
),
|
|
57
|
-
errno=err.errno,
|
|
58
|
-
)
|
|
50
|
+
if (
|
|
51
|
+
err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED
|
|
52
|
+
or "does not exist or not authorized" in err.msg
|
|
53
|
+
):
|
|
54
|
+
raise DoesNotExistOrUnauthorizedError(msg=err.msg) from err
|
|
59
55
|
elif err.errno == NO_WAREHOUSE_SELECTED_IN_SESSION:
|
|
60
|
-
raise
|
|
61
|
-
msg=dedent(
|
|
62
|
-
f"""\
|
|
63
|
-
Received error message '{err.msg}' while executing SQL statement.
|
|
64
|
-
Please provide a warehouse for the active session role in your project definition file, config.toml file, or via command line.
|
|
65
|
-
"""
|
|
66
|
-
),
|
|
67
|
-
errno=err.errno,
|
|
68
|
-
)
|
|
69
|
-
elif "does not exist or not authorized" in err.msg:
|
|
70
|
-
raise ProgrammingError(
|
|
71
|
-
msg=dedent(
|
|
72
|
-
f"""\
|
|
73
|
-
Received error message '{err.msg}' while executing SQL statement.
|
|
74
|
-
Please check the name of the resource you are trying to query or the permissions of the role you are using to run the query.
|
|
75
|
-
"""
|
|
76
|
-
)
|
|
77
|
-
)
|
|
56
|
+
raise NoWarehouseSelectedInSessionError(msg=err.msg) from err
|
|
78
57
|
raise err
|
|
79
58
|
|
|
80
59
|
|
|
81
60
|
def _get_stage_paths_to_sync(
|
|
82
61
|
local_paths_to_sync: List[Path], deploy_root: Path
|
|
83
|
-
) -> List[
|
|
62
|
+
) -> List[StagePathType]:
|
|
84
63
|
"""
|
|
85
64
|
Takes a list of paths (files and directories), returning a list of all files recursively relative to the deploy root.
|
|
86
65
|
"""
|
|
@@ -128,22 +107,15 @@ def sync_deploy_root_with_stage(
|
|
|
128
107
|
A `DiffResult` instance describing the changes that were performed.
|
|
129
108
|
"""
|
|
130
109
|
|
|
131
|
-
|
|
110
|
+
sql_facade = get_snowflake_facade()
|
|
132
111
|
# Does a stage already exist within the application package, or we need to create one?
|
|
133
112
|
# Using "if not exists" should take care of either case.
|
|
134
113
|
console.step(
|
|
135
114
|
f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
|
|
136
115
|
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
sql_executor.execute_query(
|
|
142
|
-
f"""
|
|
143
|
-
create stage if not exists {stage_fqn}
|
|
144
|
-
encryption = (TYPE = 'SNOWFLAKE_SSE')
|
|
145
|
-
DIRECTORY = (ENABLE = TRUE)"""
|
|
146
|
-
)
|
|
116
|
+
if not sql_facade.stage_exists(stage_fqn):
|
|
117
|
+
sql_facade.create_schema(stage_schema, database=package_name)
|
|
118
|
+
sql_facade.create_stage(stage_fqn)
|
|
147
119
|
|
|
148
120
|
# Perform a diff operation and display results to the user for informational purposes
|
|
149
121
|
if print_diff:
|
|
@@ -217,36 +189,24 @@ def sync_deploy_root_with_stage(
|
|
|
217
189
|
return diff
|
|
218
190
|
|
|
219
191
|
|
|
220
|
-
def _execute_sql_script(
|
|
221
|
-
script_content: str,
|
|
222
|
-
database_name: Optional[str] = None,
|
|
223
|
-
) -> None:
|
|
224
|
-
"""
|
|
225
|
-
Executing the provided SQL script content.
|
|
226
|
-
This assumes that a relevant warehouse is already active.
|
|
227
|
-
If database_name is passed in, it will be used first.
|
|
228
|
-
"""
|
|
229
|
-
try:
|
|
230
|
-
sql_executor = get_sql_executor()
|
|
231
|
-
if database_name is not None:
|
|
232
|
-
sql_executor.execute_query(f"use database {database_name}")
|
|
233
|
-
sql_executor.execute_queries(script_content)
|
|
234
|
-
except ProgrammingError as err:
|
|
235
|
-
generic_sql_error_handler(err)
|
|
236
|
-
|
|
237
|
-
|
|
238
192
|
def execute_post_deploy_hooks(
|
|
239
193
|
console: AbstractConsole,
|
|
240
194
|
project_root: Path,
|
|
241
195
|
post_deploy_hooks: Optional[List[PostDeployHook]],
|
|
242
196
|
deployed_object_type: str,
|
|
197
|
+
role_name: str,
|
|
243
198
|
database_name: str,
|
|
199
|
+
warehouse_name: str,
|
|
244
200
|
) -> None:
|
|
245
201
|
"""
|
|
246
202
|
Executes post-deploy hooks for the given object type.
|
|
247
203
|
While executing SQL post deploy hooks, it first switches to the database provided in the input.
|
|
248
204
|
All post deploy scripts templates will first be expanded using the global template context.
|
|
249
205
|
"""
|
|
206
|
+
get_cli_context().metrics.set_counter_default(
|
|
207
|
+
CLICounterField.POST_DEPLOY_SCRIPTS, 0
|
|
208
|
+
)
|
|
209
|
+
|
|
250
210
|
if not post_deploy_hooks:
|
|
251
211
|
return
|
|
252
212
|
|
|
@@ -254,9 +214,11 @@ def execute_post_deploy_hooks(
|
|
|
254
214
|
|
|
255
215
|
with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
|
|
256
216
|
sql_scripts_paths = []
|
|
217
|
+
display_paths = []
|
|
257
218
|
for hook in post_deploy_hooks:
|
|
258
219
|
if hook.sql_script:
|
|
259
220
|
sql_scripts_paths.append(hook.sql_script)
|
|
221
|
+
display_paths.append(hook.display_path)
|
|
260
222
|
else:
|
|
261
223
|
raise ValueError(
|
|
262
224
|
f"Unsupported {deployed_object_type} post-deploy hook type: {hook}"
|
|
@@ -268,11 +230,16 @@ def execute_post_deploy_hooks(
|
|
|
268
230
|
sql_scripts_paths,
|
|
269
231
|
)
|
|
270
232
|
|
|
271
|
-
|
|
233
|
+
sql_facade = get_snowflake_facade()
|
|
234
|
+
|
|
235
|
+
for index, sql_script_path in enumerate(display_paths):
|
|
272
236
|
console.step(f"Executing SQL script: {sql_script_path}")
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
237
|
+
sql_facade.execute_user_script(
|
|
238
|
+
queries=scripts_content_list[index],
|
|
239
|
+
script_name=sql_script_path,
|
|
240
|
+
role=role_name,
|
|
241
|
+
warehouse=warehouse_name,
|
|
242
|
+
database=database_name,
|
|
276
243
|
)
|
|
277
244
|
|
|
278
245
|
|
snowflake/cli/api/errno.py
CHANGED
|
@@ -26,3 +26,5 @@ ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046
|
|
|
26
26
|
NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055
|
|
27
27
|
APPLICATION_NO_LONGER_AVAILABLE = 93079
|
|
28
28
|
APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
|
|
29
|
+
APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321
|
|
30
|
+
CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
|