snowflake-cli-labs 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/secret.py +9 -0
- snowflake/cli/_app/snow_connector.py +39 -27
- snowflake/cli/_app/telemetry.py +28 -0
- snowflake/cli/_plugins/connection/commands.py +9 -4
- snowflake/cli/_plugins/git/manager.py +53 -7
- snowflake/cli/_plugins/helpers/commands.py +61 -0
- snowflake/cli/{api/project/schemas/snowpark/__init__.py → _plugins/helpers/plugin_spec.py} +17 -0
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -9
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +6 -1
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/models.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +5 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/commands.py +87 -96
- snowflake/cli/_plugins/nativeapp/entities/__init__.py +0 -0
- snowflake/cli/{api/entities/application_entity.py → _plugins/nativeapp/entities/application.py} +264 -83
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +1392 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +0 -9
- snowflake/cli/_plugins/nativeapp/manager.py +69 -185
- snowflake/cli/_plugins/nativeapp/policy.py +3 -0
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -2
- snowflake/cli/_plugins/nativeapp/run_processor.py +17 -20
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -4
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +4 -6
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +122 -88
- snowflake/cli/_plugins/nativeapp/version/commands.py +7 -39
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +46 -312
- snowflake/cli/_plugins/object/manager.py +36 -15
- snowflake/cli/_plugins/snowpark/commands.py +4 -4
- snowflake/cli/_plugins/snowpark/common.py +4 -4
- snowflake/cli/{api/entities → _plugins/snowpark}/snowpark_entity.py +2 -2
- snowflake/cli/{api/project/schemas/entities/snowpark_entity.py → _plugins/snowpark/snowpark_entity_model.py} +3 -6
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +1 -1
- snowflake/cli/_plugins/stage/manager.py +9 -4
- snowflake/cli/_plugins/streamlit/commands.py +15 -3
- snowflake/cli/_plugins/streamlit/manager.py +12 -4
- snowflake/cli/{api/entities → _plugins/streamlit}/streamlit_entity.py +2 -2
- snowflake/cli/{api/project/schemas/entities → _plugins/streamlit}/streamlit_entity_model.py +5 -12
- snowflake/cli/_plugins/workspace/commands.py +116 -36
- snowflake/cli/_plugins/workspace/plugin_spec.py +1 -1
- snowflake/cli/api/cli_global_context.py +7 -0
- snowflake/cli/api/commands/decorators.py +14 -0
- snowflake/cli/api/commands/flags.py +18 -0
- snowflake/cli/api/commands/snow_typer.py +1 -1
- snowflake/cli/api/config.py +25 -6
- snowflake/cli/api/connections.py +3 -1
- snowflake/cli/api/entities/common.py +4 -0
- snowflake/cli/api/entities/utils.py +3 -14
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/identifiers.py +4 -3
- snowflake/cli/api/metrics.py +92 -0
- snowflake/cli/api/project/definition_conversion.py +61 -18
- snowflake/cli/api/project/schemas/entities/common.py +17 -4
- snowflake/cli/api/project/schemas/entities/entities.py +11 -10
- snowflake/cli/api/project/schemas/project_definition.py +5 -7
- snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +0 -7
- snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
- snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
- snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
- snowflake/cli/api/rendering/sql_templates.py +6 -0
- snowflake/cli/api/rest_api.py +11 -5
- snowflake/cli/api/sql_execution.py +6 -15
- snowflake/cli/api/utils/definition_rendering.py +24 -4
- {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/METADATA +9 -7
- {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/RECORD +83 -79
- snowflake/cli/_plugins/nativeapp/init.py +0 -345
- snowflake/cli/api/entities/application_package_entity.py +0 -658
- snowflake/cli/api/project/schemas/entities/application_entity_model.py +0 -56
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +0 -94
- snowflake/cli/api/project/schemas/streamlit/__init__.py +0 -13
- /snowflake/cli/{api/project/schemas/native_app → _plugins/helpers}/__init__.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
- /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
- {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/licenses/LICENSE +0 -0
|
@@ -26,15 +26,15 @@ from snowflake.cli._plugins.connection.util import (
|
|
|
26
26
|
)
|
|
27
27
|
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
28
28
|
from snowflake.cli._plugins.stage.manager import StageManager
|
|
29
|
+
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
30
|
+
StreamlitEntityModel,
|
|
31
|
+
)
|
|
29
32
|
from snowflake.cli.api.commands.experimental_behaviour import (
|
|
30
33
|
experimental_behaviour_enabled,
|
|
31
34
|
)
|
|
32
35
|
from snowflake.cli.api.console import cli_console
|
|
33
36
|
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
34
37
|
from snowflake.cli.api.identifiers import FQN
|
|
35
|
-
from snowflake.cli.api.project.schemas.entities.streamlit_entity_model import (
|
|
36
|
-
StreamlitEntityModel,
|
|
37
|
-
)
|
|
38
38
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
39
39
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
40
40
|
from snowflake.connector.errors import ProgrammingError
|
|
@@ -43,6 +43,10 @@ log = logging.getLogger(__name__)
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class StreamlitManager(SqlExecutionMixin):
|
|
46
|
+
def execute(self, app_name: FQN):
|
|
47
|
+
query = f"EXECUTE STREAMLIT {app_name.sql_identifier}()"
|
|
48
|
+
return self._execute_query(query=query)
|
|
49
|
+
|
|
46
50
|
def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
|
|
47
51
|
return self._execute_query(
|
|
48
52
|
f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
|
|
@@ -98,12 +102,16 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
98
102
|
query.append(f"ROOT_LOCATION = '{from_stage_name}'")
|
|
99
103
|
|
|
100
104
|
query.append(f"MAIN_FILE = '{streamlit.main_file}'")
|
|
101
|
-
|
|
105
|
+
if streamlit.imports:
|
|
106
|
+
query.append(streamlit.get_imports_sql())
|
|
102
107
|
if streamlit.query_warehouse:
|
|
103
108
|
query.append(f"QUERY_WAREHOUSE = {streamlit.query_warehouse}")
|
|
104
109
|
if streamlit.title:
|
|
105
110
|
query.append(f"TITLE = '{streamlit.title}'")
|
|
106
111
|
|
|
112
|
+
if streamlit.comment:
|
|
113
|
+
query.append(f"COMMENT = '{streamlit.comment}'")
|
|
114
|
+
|
|
107
115
|
if streamlit.external_access_integrations:
|
|
108
116
|
query.append(streamlit.get_external_access_integrations_sql())
|
|
109
117
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from snowflake.cli.
|
|
2
|
-
from snowflake.cli.api.project.schemas.entities.streamlit_entity_model import (
|
|
1
|
+
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
3
2
|
StreamlitEntityModel,
|
|
4
3
|
)
|
|
4
|
+
from snowflake.cli.api.entities.common import EntityBase
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
@@ -20,17 +20,19 @@ from pydantic import Field, model_validator
|
|
|
20
20
|
from snowflake.cli.api.project.schemas.entities.common import (
|
|
21
21
|
EntityModelBase,
|
|
22
22
|
ExternalAccessBaseModel,
|
|
23
|
+
ImportsBaseModel,
|
|
23
24
|
)
|
|
24
25
|
from snowflake.cli.api.project.schemas.updatable_model import (
|
|
25
26
|
DiscriminatorField,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel):
|
|
30
|
+
class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel, ImportsBaseModel):
|
|
30
31
|
type: Literal["streamlit"] = DiscriminatorField() # noqa: A003
|
|
31
32
|
title: Optional[str] = Field(
|
|
32
33
|
title="Human-readable title for the Streamlit dashboard", default=None
|
|
33
34
|
)
|
|
35
|
+
comment: Optional[str] = Field(title="Comment for the Streamlit app", default=None)
|
|
34
36
|
query_warehouse: str = Field(
|
|
35
37
|
title="Snowflake warehouse to host the app", default=None
|
|
36
38
|
)
|
|
@@ -48,23 +50,14 @@ class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel):
|
|
|
48
50
|
default=None,
|
|
49
51
|
)
|
|
50
52
|
|
|
51
|
-
@model_validator(mode="after")
|
|
52
|
-
def main_file_must_be_in_artifacts(self):
|
|
53
|
-
if not self.artifacts:
|
|
54
|
-
return self
|
|
55
|
-
|
|
56
|
-
if Path(self.main_file) not in self.artifacts:
|
|
57
|
-
raise ValueError(
|
|
58
|
-
f"Specified main file {self.main_file} is not included in artifacts."
|
|
59
|
-
)
|
|
60
|
-
return self
|
|
61
|
-
|
|
62
53
|
@model_validator(mode="after")
|
|
63
54
|
def artifacts_must_exists(self):
|
|
64
55
|
if not self.artifacts:
|
|
65
56
|
return self
|
|
66
57
|
|
|
67
58
|
for artifact in self.artifacts:
|
|
59
|
+
if "*" in artifact.name:
|
|
60
|
+
continue
|
|
68
61
|
if not artifact.exists():
|
|
69
62
|
raise ValueError(
|
|
70
63
|
f"Specified artifact {artifact} does not exist locally."
|
|
@@ -20,7 +20,7 @@ from textwrap import dedent
|
|
|
20
20
|
from typing import List, Optional
|
|
21
21
|
|
|
22
22
|
import typer
|
|
23
|
-
import
|
|
23
|
+
from click import MissingParameter
|
|
24
24
|
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
25
25
|
from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
26
26
|
ForceOption,
|
|
@@ -30,50 +30,19 @@ from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
|
30
30
|
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
|
|
31
31
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
32
32
|
from snowflake.cli.api.commands.decorators import with_project_definition
|
|
33
|
-
from snowflake.cli.api.commands.snow_typer import
|
|
33
|
+
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
34
34
|
from snowflake.cli.api.entities.common import EntityActions
|
|
35
35
|
from snowflake.cli.api.exceptions import IncompatibleParametersError
|
|
36
|
-
from snowflake.cli.api.output.types import MessageResult
|
|
37
|
-
from snowflake.cli.api.project.definition_conversion import (
|
|
38
|
-
convert_project_definition_to_v2,
|
|
39
|
-
)
|
|
40
|
-
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
41
|
-
from snowflake.cli.api.secure_path import SecurePath
|
|
36
|
+
from snowflake.cli.api.output.types import MessageResult, QueryResult
|
|
42
37
|
|
|
43
|
-
ws =
|
|
38
|
+
ws = SnowTyperFactory(
|
|
44
39
|
name="ws",
|
|
45
40
|
help="Deploy and interact with snowflake.yml-based entities.",
|
|
41
|
+
is_hidden=lambda: True,
|
|
46
42
|
)
|
|
47
43
|
log = logging.getLogger(__name__)
|
|
48
44
|
|
|
49
45
|
|
|
50
|
-
@ws.command()
|
|
51
|
-
def migrate(
|
|
52
|
-
accept_templates: bool = typer.Option(
|
|
53
|
-
False, "-t", "--accept-templates", help="Allows the migration of templates."
|
|
54
|
-
),
|
|
55
|
-
**options,
|
|
56
|
-
):
|
|
57
|
-
"""Migrates the Snowpark, Streamlit, and Native App project definition files from V1 to V2."""
|
|
58
|
-
manager = DefinitionManager()
|
|
59
|
-
pd = manager.unrendered_project_definition
|
|
60
|
-
|
|
61
|
-
if pd.meets_version_requirement("2"):
|
|
62
|
-
return MessageResult("Project definition is already at version 2.")
|
|
63
|
-
|
|
64
|
-
pd_v2 = convert_project_definition_to_v2(manager.project_root, pd, accept_templates)
|
|
65
|
-
|
|
66
|
-
SecurePath("snowflake.yml").rename("snowflake_V1.yml")
|
|
67
|
-
with open("snowflake.yml", "w") as file:
|
|
68
|
-
yaml.dump(
|
|
69
|
-
pd_v2.model_dump(
|
|
70
|
-
exclude_unset=True, exclude_none=True, mode="json", by_alias=True
|
|
71
|
-
),
|
|
72
|
-
file,
|
|
73
|
-
)
|
|
74
|
-
return MessageResult("Project definition migrated to version 2.")
|
|
75
|
-
|
|
76
|
-
|
|
77
46
|
@ws.command(requires_connection=True, hidden=True)
|
|
78
47
|
@with_project_definition()
|
|
79
48
|
def bundle(
|
|
@@ -211,6 +180,8 @@ def validate(
|
|
|
211
180
|
entity_id: str = typer.Option(
|
|
212
181
|
help=f"""The ID of the entity you want to validate.""",
|
|
213
182
|
),
|
|
183
|
+
interactive: bool = InteractiveOption,
|
|
184
|
+
force: Optional[bool] = ForceOption,
|
|
214
185
|
**options,
|
|
215
186
|
):
|
|
216
187
|
"""Validates the specified entity."""
|
|
@@ -223,4 +194,113 @@ def validate(
|
|
|
223
194
|
ws.perform_action(
|
|
224
195
|
entity_id,
|
|
225
196
|
EntityActions.VALIDATE,
|
|
197
|
+
interactive=interactive,
|
|
198
|
+
force=force,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
version = SnowTyperFactory(
|
|
203
|
+
name="version",
|
|
204
|
+
help="Manages versions for project entities.",
|
|
205
|
+
)
|
|
206
|
+
ws.add_typer(version)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@version.command(name="list", requires_connection=True, hidden=True)
|
|
210
|
+
@with_project_definition()
|
|
211
|
+
def version_list(
|
|
212
|
+
entity_id: str = typer.Option(
|
|
213
|
+
help="The ID of the entity you want to list versions for.",
|
|
214
|
+
),
|
|
215
|
+
**options,
|
|
216
|
+
):
|
|
217
|
+
"""Lists the versions of the specified entity."""
|
|
218
|
+
cli_context = get_cli_context()
|
|
219
|
+
ws = WorkspaceManager(
|
|
220
|
+
project_definition=cli_context.project_definition,
|
|
221
|
+
project_root=cli_context.project_root,
|
|
222
|
+
)
|
|
223
|
+
cursor = ws.perform_action(
|
|
224
|
+
entity_id,
|
|
225
|
+
EntityActions.VERSION_LIST,
|
|
226
|
+
)
|
|
227
|
+
return QueryResult(cursor)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@version.command(name="create", requires_connection=True, hidden=True)
|
|
231
|
+
@with_project_definition()
|
|
232
|
+
def version_create(
|
|
233
|
+
entity_id: str = typer.Option(
|
|
234
|
+
help="The ID of the entity you want to create a version for.",
|
|
235
|
+
),
|
|
236
|
+
version: Optional[str] = typer.Argument(
|
|
237
|
+
None,
|
|
238
|
+
help=f"""Version to define in your application package. If the version already exists, an auto-incremented patch is added to the version instead. Defaults to the version specified in the `manifest.yml` file.""",
|
|
239
|
+
),
|
|
240
|
+
patch: Optional[int] = typer.Option(
|
|
241
|
+
None,
|
|
242
|
+
"--patch",
|
|
243
|
+
help=f"""The patch number you want to create for an existing version.
|
|
244
|
+
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
|
+
),
|
|
246
|
+
skip_git_check: Optional[bool] = typer.Option(
|
|
247
|
+
False,
|
|
248
|
+
"--skip-git-check",
|
|
249
|
+
help="When enabled, the Snowflake CLI skips checking if your project has any untracked or stages files in git. Default: unset.",
|
|
250
|
+
is_flag=True,
|
|
251
|
+
),
|
|
252
|
+
interactive: bool = InteractiveOption,
|
|
253
|
+
force: Optional[bool] = ForceOption,
|
|
254
|
+
**options,
|
|
255
|
+
):
|
|
256
|
+
"""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
|
+
|
|
260
|
+
cli_context = get_cli_context()
|
|
261
|
+
ws = WorkspaceManager(
|
|
262
|
+
project_definition=cli_context.project_definition,
|
|
263
|
+
project_root=cli_context.project_root,
|
|
264
|
+
)
|
|
265
|
+
ws.perform_action(
|
|
266
|
+
entity_id,
|
|
267
|
+
EntityActions.VERSION_CREATE,
|
|
268
|
+
version=version,
|
|
269
|
+
patch=patch,
|
|
270
|
+
skip_git_check=skip_git_check,
|
|
271
|
+
interactive=interactive,
|
|
272
|
+
force=force,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@version.command(name="drop", requires_connection=True, hidden=True)
|
|
277
|
+
@with_project_definition()
|
|
278
|
+
def version_drop(
|
|
279
|
+
entity_id: str = typer.Option(
|
|
280
|
+
help="The ID of the entity you want to create a version for.",
|
|
281
|
+
),
|
|
282
|
+
version: Optional[str] = typer.Argument(
|
|
283
|
+
None,
|
|
284
|
+
help=f"""Version to define in your application package. If the version already exists, an auto-incremented patch is added to the version instead. Defaults to the version specified in the `manifest.yml` file.""",
|
|
285
|
+
),
|
|
286
|
+
interactive: bool = InteractiveOption,
|
|
287
|
+
force: Optional[bool] = ForceOption,
|
|
288
|
+
**options,
|
|
289
|
+
):
|
|
290
|
+
"""
|
|
291
|
+
Drops a version defined for your entity. Versions can either be passed in as an argument to the command or read from the `manifest.yml` file.
|
|
292
|
+
Dropping patches is not allowed.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
cli_context = get_cli_context()
|
|
296
|
+
ws = WorkspaceManager(
|
|
297
|
+
project_definition=cli_context.project_definition,
|
|
298
|
+
project_root=cli_context.project_root,
|
|
299
|
+
)
|
|
300
|
+
ws.perform_action(
|
|
301
|
+
entity_id,
|
|
302
|
+
EntityActions.VERSION_CREATE,
|
|
303
|
+
version=version,
|
|
304
|
+
interactive=interactive,
|
|
305
|
+
force=force,
|
|
226
306
|
)
|
|
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Iterator
|
|
|
22
22
|
|
|
23
23
|
from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
|
|
24
24
|
from snowflake.cli.api.exceptions import MissingConfiguration
|
|
25
|
+
from snowflake.cli.api.metrics import CLIMetrics
|
|
25
26
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
26
27
|
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
|
|
27
28
|
from snowflake.connector import SnowflakeConnection
|
|
@@ -46,6 +47,8 @@ class _CliGlobalContextManager:
|
|
|
46
47
|
experimental: bool = False
|
|
47
48
|
enable_tracebacks: bool = True
|
|
48
49
|
|
|
50
|
+
metrics: CLIMetrics = field(default_factory=CLIMetrics)
|
|
51
|
+
|
|
49
52
|
project_path_arg: str | None = None
|
|
50
53
|
project_is_optional: bool = True
|
|
51
54
|
project_env_overrides_args: dict[str, str] = field(default_factory=dict)
|
|
@@ -152,6 +155,10 @@ class _CliGlobalContextAccess:
|
|
|
152
155
|
def enable_tracebacks(self) -> bool:
|
|
153
156
|
return self._manager.enable_tracebacks
|
|
154
157
|
|
|
158
|
+
@property
|
|
159
|
+
def metrics(self):
|
|
160
|
+
return self._manager.metrics
|
|
161
|
+
|
|
155
162
|
@property
|
|
156
163
|
def output_format(self) -> OutputFormat:
|
|
157
164
|
return self._manager.output_format
|
|
@@ -29,10 +29,12 @@ from snowflake.cli.api.commands.flags import (
|
|
|
29
29
|
DiagAllowlistPathOption,
|
|
30
30
|
DiagLogPathOption,
|
|
31
31
|
EnableDiagOption,
|
|
32
|
+
HostOption,
|
|
32
33
|
MasterTokenOption,
|
|
33
34
|
MfaPasscodeOption,
|
|
34
35
|
OutputFormatOption,
|
|
35
36
|
PasswordOption,
|
|
37
|
+
PortOption,
|
|
36
38
|
PrivateKeyPathOption,
|
|
37
39
|
RoleOption,
|
|
38
40
|
SchemaOption,
|
|
@@ -210,6 +212,18 @@ GLOBAL_CONNECTION_OPTIONS = [
|
|
|
210
212
|
annotation=Optional[str],
|
|
211
213
|
default=ConnectionOption,
|
|
212
214
|
),
|
|
215
|
+
inspect.Parameter(
|
|
216
|
+
"host",
|
|
217
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
218
|
+
annotation=Optional[str],
|
|
219
|
+
default=HostOption,
|
|
220
|
+
),
|
|
221
|
+
inspect.Parameter(
|
|
222
|
+
"port",
|
|
223
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
224
|
+
annotation=Optional[int],
|
|
225
|
+
default=PortOption,
|
|
226
|
+
),
|
|
213
227
|
inspect.Parameter(
|
|
214
228
|
"account",
|
|
215
229
|
inspect.Parameter.KEYWORD_ONLY,
|
|
@@ -100,6 +100,24 @@ TemporaryConnectionOption = typer.Option(
|
|
|
100
100
|
rich_help_panel=_CONNECTION_SECTION,
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
+
HostOption = typer.Option(
|
|
104
|
+
None,
|
|
105
|
+
"--host",
|
|
106
|
+
help="Host address for the connection. Overrides the value specified for the connection.",
|
|
107
|
+
callback=_connection_callback("host"),
|
|
108
|
+
show_default=False,
|
|
109
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
PortOption = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--port",
|
|
115
|
+
help="Port for the connection. Overrides the value specified for the connection.",
|
|
116
|
+
callback=_connection_callback("port"),
|
|
117
|
+
show_default=False,
|
|
118
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
119
|
+
)
|
|
120
|
+
|
|
103
121
|
AccountOption = typer.Option(
|
|
104
122
|
None,
|
|
105
123
|
"--account",
|
|
@@ -105,7 +105,7 @@ class SnowTyper(typer.Typer):
|
|
|
105
105
|
result = command_callable(*args, **kw)
|
|
106
106
|
self.process_result(result)
|
|
107
107
|
execution.complete(ExecutionStatus.SUCCESS)
|
|
108
|
-
except
|
|
108
|
+
except BaseException as err:
|
|
109
109
|
execution.complete(ExecutionStatus.FAILURE)
|
|
110
110
|
exception = self.exception_handler(err, execution)
|
|
111
111
|
raise exception
|
snowflake/cli/api/config.py
CHANGED
|
@@ -130,12 +130,21 @@ def config_init(config_file: Optional[Path]):
|
|
|
130
130
|
create_initial_loggers()
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
def add_connection_to_proper_file(name: str, connection_config: ConnectionConfig):
|
|
134
|
+
if CONNECTIONS_FILE.exists():
|
|
135
|
+
existing_connections = _read_connections_toml()
|
|
136
|
+
existing_connections.update(
|
|
137
|
+
{name: connection_config.to_dict_of_all_non_empty_values()}
|
|
138
|
+
)
|
|
139
|
+
_update_connections_toml(existing_connections)
|
|
140
|
+
return CONNECTIONS_FILE
|
|
141
|
+
else:
|
|
142
|
+
set_config_value(
|
|
143
|
+
section=CONNECTIONS_SECTION,
|
|
144
|
+
key=name,
|
|
145
|
+
value=connection_config.to_dict_of_all_non_empty_values(),
|
|
146
|
+
)
|
|
147
|
+
return CONFIG_MANAGER.file_path
|
|
139
148
|
|
|
140
149
|
|
|
141
150
|
_DEFAULT_LOGS_CONFIG = {
|
|
@@ -359,3 +368,13 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
|
|
|
359
368
|
return "UNKNOWN"
|
|
360
369
|
|
|
361
370
|
return {k: _bool_or_unknown(v) for k, v in flags.items()}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _read_connections_toml() -> dict:
|
|
374
|
+
with open(CONNECTIONS_FILE, "r") as f:
|
|
375
|
+
return tomlkit.loads(f.read()).unwrap()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _update_connections_toml(connections: dict):
|
|
379
|
+
with open(CONNECTIONS_FILE, "w") as f:
|
|
380
|
+
f.write(tomlkit.dumps(connections))
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -36,6 +36,8 @@ schema_pattern = re.compile(r".+\..+")
|
|
|
36
36
|
class ConnectionContext:
|
|
37
37
|
# FIXME: can reduce duplication using config.ConnectionConfig
|
|
38
38
|
connection_name: Optional[str] = None
|
|
39
|
+
host: Optional[str] = None
|
|
40
|
+
port: Optional[int] = None
|
|
39
41
|
account: Optional[str] = None
|
|
40
42
|
database: Optional[str] = None
|
|
41
43
|
role: Optional[str] = None
|
|
@@ -71,7 +73,7 @@ class ConnectionContext:
|
|
|
71
73
|
Raises KeyError if a non-property is specified as a key.
|
|
72
74
|
"""
|
|
73
75
|
field_map = {field.name: field for field in fields(self)}
|
|
74
|
-
for
|
|
76
|
+
for key, value in updates.items():
|
|
75
77
|
# ensure key represents a property
|
|
76
78
|
if key not in field_map:
|
|
77
79
|
raise KeyError(f"{key} is not a field of {self.__class__.__name__}")
|
|
@@ -9,11 +9,9 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
|
|
|
9
9
|
BundleMap,
|
|
10
10
|
resolve_without_follow,
|
|
11
11
|
)
|
|
12
|
-
from snowflake.cli._plugins.nativeapp.constants import OWNER_COL
|
|
13
12
|
from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
14
13
|
InvalidTemplateInFileError,
|
|
15
14
|
MissingScriptError,
|
|
16
|
-
UnexpectedOwnerError,
|
|
17
15
|
)
|
|
18
16
|
from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
|
|
19
17
|
from snowflake.cli._plugins.stage.diff import (
|
|
@@ -33,8 +31,8 @@ from snowflake.cli.api.errno import (
|
|
|
33
31
|
NO_WAREHOUSE_SELECTED_IN_SESSION,
|
|
34
32
|
)
|
|
35
33
|
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
|
|
34
|
+
from snowflake.cli.api.metrics import CLICounterField
|
|
36
35
|
from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
|
|
37
|
-
from snowflake.cli.api.project.util import unquote_identifier
|
|
38
36
|
from snowflake.cli.api.rendering.sql_templates import (
|
|
39
37
|
choose_sql_jinja_env_based_on_template_syntax,
|
|
40
38
|
)
|
|
@@ -80,17 +78,6 @@ def generic_sql_error_handler(
|
|
|
80
78
|
raise err
|
|
81
79
|
|
|
82
80
|
|
|
83
|
-
def ensure_correct_owner(row: dict, role: str, obj_name: str) -> None:
|
|
84
|
-
"""
|
|
85
|
-
Check if an object has the right owner role
|
|
86
|
-
"""
|
|
87
|
-
actual_owner = row[
|
|
88
|
-
OWNER_COL
|
|
89
|
-
].upper() # Because unquote_identifier() always returns uppercase str
|
|
90
|
-
if actual_owner != unquote_identifier(role):
|
|
91
|
-
raise UnexpectedOwnerError(obj_name, role, actual_owner)
|
|
92
|
-
|
|
93
|
-
|
|
94
81
|
def _get_stage_paths_to_sync(
|
|
95
82
|
local_paths_to_sync: List[Path], deploy_root: Path
|
|
96
83
|
) -> List[StagePath]:
|
|
@@ -263,6 +250,8 @@ def execute_post_deploy_hooks(
|
|
|
263
250
|
if not post_deploy_hooks:
|
|
264
251
|
return
|
|
265
252
|
|
|
253
|
+
get_cli_context().metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 1)
|
|
254
|
+
|
|
266
255
|
with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
|
|
267
256
|
sql_scripts_paths = []
|
|
268
257
|
for hook in post_deploy_hooks:
|
snowflake/cli/api/errno.py
CHANGED
snowflake/cli/api/identifiers.py
CHANGED
|
@@ -18,8 +18,7 @@ import re
|
|
|
18
18
|
|
|
19
19
|
from click import ClickException
|
|
20
20
|
from snowflake.cli.api.exceptions import FQNInconsistencyError, FQNNameError
|
|
21
|
-
from snowflake.cli.api.project.schemas.identifier_model import (
|
|
22
|
-
Identifier,
|
|
21
|
+
from snowflake.cli.api.project.schemas.v1.identifier_model import (
|
|
23
22
|
ObjectIdentifierBaseModel,
|
|
24
23
|
)
|
|
25
24
|
from snowflake.cli.api.project.util import VALID_IDENTIFIER_REGEX, identifier_for_url
|
|
@@ -142,8 +141,10 @@ class FQN:
|
|
|
142
141
|
return fqn.set_database(model.database).set_schema(model.schema_name)
|
|
143
142
|
|
|
144
143
|
@classmethod
|
|
145
|
-
def from_identifier_model_v2(cls, model
|
|
144
|
+
def from_identifier_model_v2(cls, model) -> "FQN":
|
|
146
145
|
"""Create an instance from object model."""
|
|
146
|
+
from snowflake.cli.api.project.schemas.entities.common import Identifier
|
|
147
|
+
|
|
147
148
|
if not isinstance(model, Identifier):
|
|
148
149
|
raise ClickException(f"Expected {type(Identifier).__name__}, got {model}.")
|
|
149
150
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _TypePrefix:
|
|
19
|
+
FEATURES = "features"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _DomainPrefix:
|
|
23
|
+
GLOBAL = "global"
|
|
24
|
+
APP = "app"
|
|
25
|
+
SQL = "sql"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CLICounterField:
|
|
29
|
+
"""
|
|
30
|
+
for each counter field we're adopting a convention of
|
|
31
|
+
<type>.<domain>.<name>
|
|
32
|
+
for example, if we're tracking a global feature, then the field name would be
|
|
33
|
+
features.global.feature_name
|
|
34
|
+
|
|
35
|
+
The metrics API is implemented to be generic, but we are adopting a convention
|
|
36
|
+
for feature tracking with the following model for a given command execution:
|
|
37
|
+
* counter not present -> feature is not available
|
|
38
|
+
* counter == 0 -> feature is available, but not used
|
|
39
|
+
* counter == 1 -> feature is used
|
|
40
|
+
this makes it easy to compute percentages for feature dashboards in Snowsight
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
TEMPLATES_PROCESSOR = (
|
|
44
|
+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.templates_processor"
|
|
45
|
+
)
|
|
46
|
+
SQL_TEMPLATES = f"{_TypePrefix.FEATURES}.{_DomainPrefix.SQL}.sql_templates"
|
|
47
|
+
PDF_TEMPLATES = f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.pdf_templates"
|
|
48
|
+
SNOWPARK_PROCESSOR = (
|
|
49
|
+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.snowpark_processor"
|
|
50
|
+
)
|
|
51
|
+
POST_DEPLOY_SCRIPTS = (
|
|
52
|
+
f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.post_deploy_scripts"
|
|
53
|
+
)
|
|
54
|
+
PACKAGE_SCRIPTS = f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.package_scripts"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CLIMetrics:
|
|
58
|
+
"""
|
|
59
|
+
Class to track various metrics across the execution of a command
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self._counters: Dict[str, int] = {}
|
|
64
|
+
|
|
65
|
+
def __eq__(self, other):
|
|
66
|
+
if isinstance(other, CLIMetrics):
|
|
67
|
+
return self._counters == other._counters
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def get_counter(self, name: str) -> Optional[int]:
|
|
71
|
+
return self._counters.get(name)
|
|
72
|
+
|
|
73
|
+
def set_counter(self, name: str, value: int) -> None:
|
|
74
|
+
self._counters[name] = value
|
|
75
|
+
|
|
76
|
+
def set_counter_default(self, name: str, value: int) -> None:
|
|
77
|
+
"""
|
|
78
|
+
sets the counter if it does not already exist
|
|
79
|
+
"""
|
|
80
|
+
if name not in self._counters:
|
|
81
|
+
self.set_counter(name, value)
|
|
82
|
+
|
|
83
|
+
def increment_counter(self, name: str, value: int = 1) -> None:
|
|
84
|
+
if name not in self._counters:
|
|
85
|
+
self.set_counter(name, value)
|
|
86
|
+
else:
|
|
87
|
+
self._counters[name] += value
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def counters(self) -> Dict[str, int]:
|
|
91
|
+
# return a copy of the original dict to avoid mutating the original
|
|
92
|
+
return self._counters.copy()
|