snowflake-cli-labs 2.6.1__py3-none-any.whl → 2.7.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/api/cli_global_context.py +9 -0
- snowflake/cli/api/commands/decorators.py +9 -4
- snowflake/cli/api/commands/execution_metadata.py +40 -0
- snowflake/cli/api/commands/flags.py +45 -36
- snowflake/cli/api/commands/project_initialisation.py +5 -2
- snowflake/cli/api/commands/snow_typer.py +20 -9
- snowflake/cli/api/errno.py +27 -0
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/identifiers.py +20 -3
- snowflake/cli/api/output/types.py +9 -0
- snowflake/cli/api/project/definition_manager.py +2 -2
- snowflake/cli/api/project/project_verification.py +23 -0
- snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
- snowflake/cli/api/project/schemas/entities/common.py +85 -0
- snowflake/cli/api/project/schemas/entities/entities.py +30 -0
- snowflake/cli/api/project/schemas/project_definition.py +114 -22
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
- snowflake/cli/api/project/schemas/template.py +77 -0
- snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
- snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
- snowflake/cli/api/rendering/project_definition_templates.py +39 -0
- snowflake/cli/api/rendering/project_templates.py +97 -0
- snowflake/cli/api/rendering/sql_templates.py +56 -0
- snowflake/cli/api/sql_execution.py +40 -1
- snowflake/cli/api/utils/definition_rendering.py +8 -5
- snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
- snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
- snowflake/cli/app/loggers.py +3 -1
- snowflake/cli/app/printing.py +17 -7
- snowflake/cli/app/snow_connector.py +9 -1
- snowflake/cli/app/telemetry.py +41 -2
- snowflake/cli/plugins/connection/commands.py +4 -3
- snowflake/cli/plugins/connection/util.py +73 -18
- snowflake/cli/plugins/cortex/commands.py +2 -1
- snowflake/cli/plugins/git/commands.py +20 -4
- snowflake/cli/plugins/git/manager.py +44 -20
- snowflake/cli/plugins/init/__init__.py +13 -0
- snowflake/cli/plugins/init/commands.py +242 -0
- snowflake/cli/plugins/init/plugin_spec.py +30 -0
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
- snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
- snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
- snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
- snowflake/cli/plugins/nativeapp/commands.py +69 -6
- snowflake/cli/plugins/nativeapp/constants.py +0 -6
- snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
- snowflake/cli/plugins/nativeapp/init.py +1 -1
- snowflake/cli/plugins/nativeapp/manager.py +114 -39
- snowflake/cli/plugins/nativeapp/project_model.py +8 -4
- snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
- snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
- snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
- snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
- snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
- snowflake/cli/plugins/snowpark/commands.py +34 -26
- snowflake/cli/plugins/snowpark/common.py +88 -27
- snowflake/cli/plugins/snowpark/manager.py +16 -5
- snowflake/cli/plugins/snowpark/models.py +6 -0
- snowflake/cli/plugins/sql/commands.py +3 -5
- snowflake/cli/plugins/sql/manager.py +1 -1
- snowflake/cli/plugins/stage/commands.py +2 -2
- snowflake/cli/plugins/stage/diff.py +4 -2
- snowflake/cli/plugins/stage/manager.py +290 -86
- snowflake/cli/plugins/streamlit/commands.py +20 -6
- snowflake/cli/plugins/streamlit/manager.py +29 -27
- snowflake/cli/plugins/workspace/__init__.py +13 -0
- snowflake/cli/plugins/workspace/commands.py +35 -0
- snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
- snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
- snowflake/cli/templates/default_snowpark/app/common.py +0 -15
- snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
- snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
- snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
- snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
- snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
- {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/METADATA +7 -6
- {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/RECORD +84 -64
- {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC
|
|
18
|
+
from typing import Generic, List, Optional, TypeVar
|
|
19
|
+
|
|
20
|
+
from pydantic import AliasChoices, Field, GetCoreSchemaHandler, ValidationInfo
|
|
21
|
+
from pydantic_core import core_schema
|
|
22
|
+
from snowflake.cli.api.project.schemas.native_app.application import (
|
|
23
|
+
ApplicationPostDeployHook,
|
|
24
|
+
)
|
|
25
|
+
from snowflake.cli.api.project.schemas.updatable_model import (
|
|
26
|
+
IdentifierField,
|
|
27
|
+
UpdatableModel,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MetaField(UpdatableModel):
|
|
32
|
+
warehouse: Optional[str] = IdentifierField(
|
|
33
|
+
title="Warehouse used to run the scripts", default=None
|
|
34
|
+
)
|
|
35
|
+
role: Optional[str] = IdentifierField(
|
|
36
|
+
title="Role to use when creating the entity object",
|
|
37
|
+
default=None,
|
|
38
|
+
)
|
|
39
|
+
post_deploy: Optional[List[ApplicationPostDeployHook]] = Field(
|
|
40
|
+
title="Actions that will be executed after the application object is created/upgraded",
|
|
41
|
+
default=None,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DefaultsField(UpdatableModel):
|
|
46
|
+
schema_: Optional[str] = Field(
|
|
47
|
+
title="Schema.",
|
|
48
|
+
validation_alias=AliasChoices("schema"),
|
|
49
|
+
default=None,
|
|
50
|
+
)
|
|
51
|
+
stage: Optional[str] = Field(
|
|
52
|
+
title="Stage.",
|
|
53
|
+
default=None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EntityBase(ABC, UpdatableModel):
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_type(cls) -> str:
|
|
60
|
+
return cls.model_fields["type"].annotation.__args__[0]
|
|
61
|
+
|
|
62
|
+
meta: Optional[MetaField] = Field(title="Meta fields", default=None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
TargetType = TypeVar("TargetType")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TargetField(Generic[TargetType]):
|
|
69
|
+
def __init__(self, entity_target_key: str):
|
|
70
|
+
self.value = entity_target_key
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return self.value
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def validate(cls, value: str, info: ValidationInfo) -> TargetField:
|
|
77
|
+
return cls(value)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def __get_pydantic_core_schema__(
|
|
81
|
+
cls, source_type, handler: GetCoreSchemaHandler
|
|
82
|
+
) -> core_schema.CoreSchema:
|
|
83
|
+
return core_schema.with_info_after_validator_function(
|
|
84
|
+
cls.validate, handler(str), field_name=handler.field_name
|
|
85
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Union, get_args
|
|
18
|
+
|
|
19
|
+
from snowflake.cli.api.project.schemas.entities.application_entity import (
|
|
20
|
+
ApplicationEntity,
|
|
21
|
+
)
|
|
22
|
+
from snowflake.cli.api.project.schemas.entities.application_package_entity import (
|
|
23
|
+
ApplicationPackageEntity,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
Entity = Union[ApplicationEntity, ApplicationPackageEntity]
|
|
27
|
+
|
|
28
|
+
ALL_ENTITIES = [*get_args(Entity)]
|
|
29
|
+
|
|
30
|
+
v2_entity_types_map = {e.get_type(): e for e in ALL_ENTITIES}
|
|
@@ -15,17 +15,30 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import Dict, Optional, Union
|
|
19
19
|
|
|
20
20
|
from packaging.version import Version
|
|
21
|
-
from pydantic import Field, ValidationError, field_validator
|
|
21
|
+
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
22
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
22
23
|
from snowflake.cli.api.project.errors import SchemaValidationError
|
|
24
|
+
from snowflake.cli.api.project.schemas.entities.application_entity import (
|
|
25
|
+
ApplicationEntity,
|
|
26
|
+
)
|
|
27
|
+
from snowflake.cli.api.project.schemas.entities.common import (
|
|
28
|
+
DefaultsField,
|
|
29
|
+
TargetField,
|
|
30
|
+
)
|
|
31
|
+
from snowflake.cli.api.project.schemas.entities.entities import (
|
|
32
|
+
Entity,
|
|
33
|
+
v2_entity_types_map,
|
|
34
|
+
)
|
|
23
35
|
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
|
|
24
36
|
from snowflake.cli.api.project.schemas.snowpark.snowpark import Snowpark
|
|
25
37
|
from snowflake.cli.api.project.schemas.streamlit.streamlit import Streamlit
|
|
26
38
|
from snowflake.cli.api.project.schemas.updatable_model import UpdatableModel
|
|
27
39
|
from snowflake.cli.api.utils.models import ProjectEnvironment
|
|
28
40
|
from snowflake.cli.api.utils.types import Context
|
|
41
|
+
from typing_extensions import Annotated
|
|
29
42
|
|
|
30
43
|
|
|
31
44
|
@dataclass
|
|
@@ -46,7 +59,7 @@ class ProjectProperties:
|
|
|
46
59
|
project_context: Context
|
|
47
60
|
|
|
48
61
|
|
|
49
|
-
class
|
|
62
|
+
class _ProjectDefinitionBase(UpdatableModel):
|
|
50
63
|
def __init__(self, *args, **kwargs):
|
|
51
64
|
try:
|
|
52
65
|
super().__init__(**kwargs)
|
|
@@ -61,9 +74,10 @@ class _BaseDefinition(UpdatableModel):
|
|
|
61
74
|
@classmethod
|
|
62
75
|
def _is_supported_version(cls, version: str) -> str:
|
|
63
76
|
version = str(version)
|
|
64
|
-
|
|
77
|
+
version_map = get_version_map()
|
|
78
|
+
if version not in version_map:
|
|
65
79
|
raise ValueError(
|
|
66
|
-
f'Version {version} is not supported. Supported versions: {", ".join(
|
|
80
|
+
f'Version {version} is not supported. Supported versions: {", ".join(version_map)}'
|
|
67
81
|
)
|
|
68
82
|
return version
|
|
69
83
|
|
|
@@ -71,7 +85,7 @@ class _BaseDefinition(UpdatableModel):
|
|
|
71
85
|
return Version(self.definition_version) >= Version(required_version)
|
|
72
86
|
|
|
73
87
|
|
|
74
|
-
class
|
|
88
|
+
class DefinitionV10(_ProjectDefinitionBase):
|
|
75
89
|
native_app: Optional[NativeApp] = Field(
|
|
76
90
|
title="Native app definitions for the project", default=None
|
|
77
91
|
)
|
|
@@ -84,7 +98,7 @@ class _DefinitionV10(_BaseDefinition):
|
|
|
84
98
|
)
|
|
85
99
|
|
|
86
100
|
|
|
87
|
-
class
|
|
101
|
+
class DefinitionV11(DefinitionV10):
|
|
88
102
|
env: Union[Dict[str, str], ProjectEnvironment, None] = Field(
|
|
89
103
|
title="Environment specification for this project.",
|
|
90
104
|
default=None,
|
|
@@ -102,23 +116,101 @@ class _DefinitionV11(_DefinitionV10):
|
|
|
102
116
|
return ProjectEnvironment(default_env=(env or {}), override_env={})
|
|
103
117
|
|
|
104
118
|
|
|
105
|
-
class
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
class DefinitionV20(_ProjectDefinitionBase):
|
|
120
|
+
entities: Dict[str, Annotated[Entity, Field(discriminator="type")]] = Field(
|
|
121
|
+
title="Entity definitions."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@model_validator(mode="before")
|
|
125
|
+
@classmethod
|
|
126
|
+
def apply_defaults(cls, data: Dict) -> Dict:
|
|
127
|
+
"""
|
|
128
|
+
Applies default values that exist on the model but not specified in yml
|
|
129
|
+
"""
|
|
130
|
+
if "defaults" in data and "entities" in data:
|
|
131
|
+
for key, entity in data["entities"].items():
|
|
132
|
+
entity_type = entity["type"]
|
|
133
|
+
if entity_type not in v2_entity_types_map:
|
|
134
|
+
continue
|
|
135
|
+
entity_model = v2_entity_types_map[entity_type]
|
|
136
|
+
for default_key, default_value in data["defaults"].items():
|
|
137
|
+
if (
|
|
138
|
+
default_key in entity_model.model_fields
|
|
139
|
+
and default_key not in entity
|
|
140
|
+
):
|
|
141
|
+
entity[default_key] = default_value
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
@field_validator("entities", mode="after")
|
|
145
|
+
@classmethod
|
|
146
|
+
def validate_entities(cls, entities: Dict[str, Entity]) -> Dict[str, Entity]:
|
|
147
|
+
for key, entity in entities.items():
|
|
148
|
+
# TODO Automatically detect TargetFields to validate
|
|
149
|
+
if entity.type == ApplicationEntity.get_type():
|
|
150
|
+
if isinstance(entity.from_.target, TargetField):
|
|
151
|
+
target_key = str(entity.from_.target)
|
|
152
|
+
target_class = entity.from_.__class__.model_fields["target"]
|
|
153
|
+
target_type = target_class.annotation.__args__[0]
|
|
154
|
+
cls._validate_target_field(target_key, target_type, entities)
|
|
155
|
+
return entities
|
|
109
156
|
|
|
110
|
-
@
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
@classmethod
|
|
158
|
+
def _validate_target_field(
|
|
159
|
+
cls, target_key: str, target_type: Entity, entities: Dict[str, Entity]
|
|
160
|
+
):
|
|
161
|
+
if target_key not in entities:
|
|
162
|
+
raise ValueError(f"No such target: {target_key}")
|
|
163
|
+
else:
|
|
164
|
+
# Validate the target type
|
|
165
|
+
actual_target_type = entities[target_key].__class__
|
|
166
|
+
if target_type and target_type is not actual_target_type:
|
|
117
167
|
raise ValueError(
|
|
118
|
-
f"
|
|
168
|
+
f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
|
|
119
169
|
)
|
|
120
|
-
version_model(**data)
|
|
121
170
|
|
|
171
|
+
defaults: Optional[DefaultsField] = Field(
|
|
172
|
+
title="Default key/value entity values that are merged recursively for each entity.",
|
|
173
|
+
default=None,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
env: Union[Dict[str, str], ProjectEnvironment, None] = Field(
|
|
177
|
+
title="Environment specification for this project.",
|
|
178
|
+
default=None,
|
|
179
|
+
validation_alias="env",
|
|
180
|
+
union_mode="smart",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@field_validator("env")
|
|
184
|
+
@classmethod
|
|
185
|
+
def _convert_env(
|
|
186
|
+
cls, env: Union[Dict, ProjectEnvironment, None]
|
|
187
|
+
) -> ProjectEnvironment:
|
|
188
|
+
if isinstance(env, ProjectEnvironment):
|
|
189
|
+
return env
|
|
190
|
+
return ProjectEnvironment(default_env=(env or {}), override_env={})
|
|
122
191
|
|
|
123
|
-
|
|
124
|
-
|
|
192
|
+
|
|
193
|
+
def build_project_definition(**data):
|
|
194
|
+
"""
|
|
195
|
+
Returns a ProjectDefinition instance with a version matching the provided definition_version value
|
|
196
|
+
"""
|
|
197
|
+
if not isinstance(data, dict):
|
|
198
|
+
return
|
|
199
|
+
version = data.get("definition_version")
|
|
200
|
+
version_model = get_version_map().get(str(version))
|
|
201
|
+
if not version or not version_model:
|
|
202
|
+
# Raises a SchemaValidationError
|
|
203
|
+
_ProjectDefinitionBase(**data)
|
|
204
|
+
return version_model(**data)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
ProjectDefinitionV1 = Union[DefinitionV10, DefinitionV11]
|
|
208
|
+
ProjectDefinitionV2 = DefinitionV20
|
|
209
|
+
ProjectDefinition = Union[ProjectDefinitionV1, ProjectDefinitionV2]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_version_map():
|
|
213
|
+
version_map = {"1": DefinitionV10, "1.1": DefinitionV11}
|
|
214
|
+
if FeatureFlag.ENABLE_PROJECT_DEFINITION_V2.is_enabled():
|
|
215
|
+
version_map["2"] = DefinitionV20
|
|
216
|
+
return version_map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
from typing import List, Optional
|
|
18
19
|
|
|
19
20
|
from pydantic import Field
|
|
@@ -28,15 +29,15 @@ class Streamlit(UpdatableModel, ObjectIdentifierModel(object_name="Streamlit")):
|
|
|
28
29
|
query_warehouse: str = Field(
|
|
29
30
|
title="Snowflake warehouse to host the app", default="streamlit"
|
|
30
31
|
)
|
|
31
|
-
main_file: Optional[
|
|
32
|
+
main_file: Optional[Path] = Field(
|
|
32
33
|
title="Entrypoint file of the Streamlit app", default="streamlit_app.py"
|
|
33
34
|
)
|
|
34
|
-
env_file: Optional[
|
|
35
|
+
env_file: Optional[Path] = Field(
|
|
35
36
|
title="File defining additional configurations for the app, such as external dependencies",
|
|
36
37
|
default=None,
|
|
37
38
|
)
|
|
38
|
-
pages_dir: Optional[
|
|
39
|
-
additional_source_files: Optional[List[
|
|
39
|
+
pages_dir: Optional[Path] = Field(title="Streamlit pages", default=None)
|
|
40
|
+
additional_source_files: Optional[List[Path]] = Field(
|
|
40
41
|
title="List of additional files which should be included into deployment artifacts",
|
|
41
42
|
default=None,
|
|
42
43
|
)
|
|
@@ -0,0 +1,77 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, List, Literal, Optional, Union
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from click import ClickException
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
from snowflake.cli.api.exceptions import InvalidTemplate
|
|
23
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TemplateVariable(BaseModel):
|
|
27
|
+
name: str = Field(..., title="Variable identifier")
|
|
28
|
+
type: Optional[Literal["string", "float", "int"]] = Field( # noqa: A003
|
|
29
|
+
title="Type of the variable", default=None
|
|
30
|
+
)
|
|
31
|
+
prompt: Optional[str] = Field(title="Prompt message for the variable", default=None)
|
|
32
|
+
default: Optional[Any] = Field(title="Default value of the variable", default=None)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def python_type(self):
|
|
36
|
+
# override "unchecked type" (None) with 'str', as Typer deduces type from the value of 'default'
|
|
37
|
+
return {
|
|
38
|
+
"string": str,
|
|
39
|
+
"float": float,
|
|
40
|
+
"int": int,
|
|
41
|
+
None: str,
|
|
42
|
+
}[self.type]
|
|
43
|
+
|
|
44
|
+
def prompt_user_for_value(self, no_interactive: bool) -> Union[str, float, int]:
|
|
45
|
+
if no_interactive:
|
|
46
|
+
if not self.default:
|
|
47
|
+
raise ClickException(f"Cannot determine value of variable {self.name}")
|
|
48
|
+
return self.default
|
|
49
|
+
|
|
50
|
+
prompt = self.prompt if self.prompt else self.name
|
|
51
|
+
return typer.prompt(prompt, default=self.default, type=self.python_type)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Template(BaseModel):
|
|
55
|
+
minimum_cli_version: Optional[str] = Field(
|
|
56
|
+
None, title="Minimum version of Snowflake CLI supporting this template"
|
|
57
|
+
)
|
|
58
|
+
files_to_render: List[str] = Field(title="List of files to be rendered", default=[])
|
|
59
|
+
variables: List[TemplateVariable] = Field(
|
|
60
|
+
title="List of variables to be rendered", default=[]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __init__(self, template_root: SecurePath, **kwargs):
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
self._validate_files_exist(template_root)
|
|
66
|
+
|
|
67
|
+
def _validate_files_exist(self, template_root: SecurePath) -> None:
|
|
68
|
+
for path_in_template in self.files_to_render:
|
|
69
|
+
full_path = template_root / path_in_template
|
|
70
|
+
if not full_path.exists():
|
|
71
|
+
raise InvalidTemplate(
|
|
72
|
+
f"[files_to_render] contains not-existing file: {path_in_template}"
|
|
73
|
+
)
|
|
74
|
+
if full_path.is_dir():
|
|
75
|
+
raise InvalidTemplate(
|
|
76
|
+
f"[files_to_render] contains a dictionary: {path_in_template}"
|
|
77
|
+
)
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
from __future__ import annotations
|
|
16
17
|
|
|
17
18
|
from pathlib import Path
|
|
@@ -19,14 +20,10 @@ from textwrap import dedent
|
|
|
19
20
|
from typing import Dict, Optional
|
|
20
21
|
|
|
21
22
|
import jinja2
|
|
22
|
-
from click import ClickException
|
|
23
23
|
from jinja2 import Environment, StrictUndefined, loaders
|
|
24
|
-
from snowflake.cli.api.cli_global_context import cli_context
|
|
25
24
|
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
26
25
|
|
|
27
26
|
CONTEXT_KEY = "ctx"
|
|
28
|
-
_YML_TEMPLATE_START = "<%"
|
|
29
|
-
_YML_TEMPLATE_END = "%>"
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
def read_file_content(file_name: str):
|
|
@@ -56,7 +53,7 @@ def procedure_from_js_file(env: jinja2.Environment, file_name: str):
|
|
|
56
53
|
_CUSTOM_FILTERS = [read_file_content, procedure_from_js_file]
|
|
57
54
|
|
|
58
55
|
|
|
59
|
-
def
|
|
56
|
+
def env_bootstrap(env: Environment) -> Environment:
|
|
60
57
|
for custom_filter in _CUSTOM_FILTERS:
|
|
61
58
|
env.filters[custom_filter.__name__] = custom_filter
|
|
62
59
|
|
|
@@ -84,36 +81,6 @@ class IgnoreAttrEnvironment(Environment):
|
|
|
84
81
|
return self.undefined(obj=obj, name=argument)
|
|
85
82
|
|
|
86
83
|
|
|
87
|
-
def get_snowflake_cli_jinja_env() -> Environment:
|
|
88
|
-
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
89
|
-
return _env_bootstrap(
|
|
90
|
-
IgnoreAttrEnvironment(
|
|
91
|
-
loader=loaders.BaseLoader(),
|
|
92
|
-
keep_trailing_newline=True,
|
|
93
|
-
variable_start_string=_YML_TEMPLATE_START,
|
|
94
|
-
variable_end_string=_YML_TEMPLATE_END,
|
|
95
|
-
block_start_string=_random_block,
|
|
96
|
-
block_end_string=_random_block,
|
|
97
|
-
undefined=StrictUndefined,
|
|
98
|
-
)
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def get_sql_cli_jinja_env():
|
|
103
|
-
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
104
|
-
return _env_bootstrap(
|
|
105
|
-
IgnoreAttrEnvironment(
|
|
106
|
-
loader=loaders.BaseLoader(),
|
|
107
|
-
keep_trailing_newline=True,
|
|
108
|
-
variable_start_string="&{",
|
|
109
|
-
variable_end_string="}",
|
|
110
|
-
block_start_string=_random_block,
|
|
111
|
-
block_end_string=_random_block,
|
|
112
|
-
undefined=StrictUndefined,
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
84
|
def jinja_render_from_file(
|
|
118
85
|
template_path: Path, data: Dict, output_file_path: Optional[Path] = None
|
|
119
86
|
) -> Optional[str]:
|
|
@@ -128,7 +95,7 @@ def jinja_render_from_file(
|
|
|
128
95
|
Returns:
|
|
129
96
|
None if file path is provided, else returns the rendered string.
|
|
130
97
|
"""
|
|
131
|
-
env =
|
|
98
|
+
env = env_bootstrap(
|
|
132
99
|
IgnoreAttrEnvironment(
|
|
133
100
|
loader=loaders.FileSystemLoader(template_path.parent),
|
|
134
101
|
keep_trailing_newline=True,
|
|
@@ -142,15 +109,3 @@ def jinja_render_from_file(
|
|
|
142
109
|
return None
|
|
143
110
|
else:
|
|
144
111
|
return rendered_result
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
|
|
148
|
-
data = data or {}
|
|
149
|
-
if CONTEXT_KEY in data:
|
|
150
|
-
raise ClickException(
|
|
151
|
-
f"{CONTEXT_KEY} in user defined data. The `{CONTEXT_KEY}` variable is reserved for CLI usage."
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
context_data = cli_context.template_context
|
|
155
|
-
context_data.update(data)
|
|
156
|
-
return get_sql_cli_jinja_env().from_string(content).render(**context_data)
|
|
@@ -0,0 +1,39 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from jinja2 import Environment, StrictUndefined, loaders
|
|
18
|
+
from snowflake.cli.api.rendering.jinja import (
|
|
19
|
+
IgnoreAttrEnvironment,
|
|
20
|
+
env_bootstrap,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_YML_TEMPLATE_START = "<%"
|
|
24
|
+
_YML_TEMPLATE_END = "%>"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_project_definition_cli_jinja_env() -> Environment:
|
|
28
|
+
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
29
|
+
return env_bootstrap(
|
|
30
|
+
IgnoreAttrEnvironment(
|
|
31
|
+
loader=loaders.BaseLoader(),
|
|
32
|
+
keep_trailing_newline=True,
|
|
33
|
+
variable_start_string=_YML_TEMPLATE_START,
|
|
34
|
+
variable_end_string=_YML_TEMPLATE_END,
|
|
35
|
+
block_start_string=_random_block,
|
|
36
|
+
block_end_string=_random_block,
|
|
37
|
+
undefined=StrictUndefined,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from click import ClickException
|
|
20
|
+
from jinja2 import (
|
|
21
|
+
Environment,
|
|
22
|
+
StrictUndefined,
|
|
23
|
+
TemplateSyntaxError,
|
|
24
|
+
UndefinedError,
|
|
25
|
+
loaders,
|
|
26
|
+
)
|
|
27
|
+
from snowflake.cli.api.exceptions import InvalidTemplate
|
|
28
|
+
from snowflake.cli.api.rendering.jinja import IgnoreAttrEnvironment, env_bootstrap
|
|
29
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
30
|
+
|
|
31
|
+
_PROJECT_TEMPLATE_START = "<!"
|
|
32
|
+
_PROJECT_TEMPLATE_END = "!>"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_snowflake_identifier(value: Optional[str]) -> Optional[str]:
|
|
36
|
+
if not value:
|
|
37
|
+
# passing "None" through filter to allow jinja to handle "undefined value" exception
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
# TODO: remove code duplication when joining "init" with "snow app init"
|
|
43
|
+
# See https://docs.snowflake.com/en/sql-reference/identifiers-syntax for identifier syntax
|
|
44
|
+
unquoted_identifier_regex = r"([a-zA-Z_])([a-zA-Z0-9_$]{0,254})"
|
|
45
|
+
quoted_identifier_regex = r'"((""|[^"]){0,255})"'
|
|
46
|
+
|
|
47
|
+
if re.fullmatch(quoted_identifier_regex, value):
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
result = re.sub(r"[. -]+", "_", value)
|
|
51
|
+
if not re.fullmatch(unquoted_identifier_regex, result):
|
|
52
|
+
raise ClickException(
|
|
53
|
+
f"Value '{value}' cannot be converted to valid Snowflake identifier."
|
|
54
|
+
' Consider enclosing it in double quotes: ""'
|
|
55
|
+
)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
PROJECT_TEMPLATE_FILTERS = [to_snowflake_identifier]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_template_cli_jinja_env(template_root: SecurePath) -> Environment:
|
|
63
|
+
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
64
|
+
env = env_bootstrap(
|
|
65
|
+
IgnoreAttrEnvironment(
|
|
66
|
+
loader=loaders.FileSystemLoader(searchpath=template_root.path),
|
|
67
|
+
keep_trailing_newline=True,
|
|
68
|
+
variable_start_string=_PROJECT_TEMPLATE_START,
|
|
69
|
+
variable_end_string=_PROJECT_TEMPLATE_END,
|
|
70
|
+
block_start_string=_random_block,
|
|
71
|
+
block_end_string=_random_block,
|
|
72
|
+
undefined=StrictUndefined,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
env.filters["to_snowflake_identifier"] = to_snowflake_identifier
|
|
76
|
+
|
|
77
|
+
return env
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_template_files(
|
|
81
|
+
template_root: SecurePath, files_to_render: List[str], data: Dict[str, Any]
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Override all listed files with their rendered version."""
|
|
84
|
+
jinja_env = get_template_cli_jinja_env(template_root)
|
|
85
|
+
for path in files_to_render:
|
|
86
|
+
try:
|
|
87
|
+
jinja_template = jinja_env.get_template(path)
|
|
88
|
+
rendered_result = jinja_template.render(**data)
|
|
89
|
+
full_path = template_root / path
|
|
90
|
+
full_path.write_text(rendered_result)
|
|
91
|
+
except TemplateSyntaxError as err:
|
|
92
|
+
raise InvalidTemplate(
|
|
93
|
+
f"Invalid template syntax in line {err.lineno} of file {path}:\n"
|
|
94
|
+
f"{err.message}"
|
|
95
|
+
)
|
|
96
|
+
except UndefinedError as err:
|
|
97
|
+
raise InvalidTemplate(err.message)
|