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.
Files changed (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/cli_global_context.py +9 -0
  3. snowflake/cli/api/commands/decorators.py +9 -4
  4. snowflake/cli/api/commands/execution_metadata.py +40 -0
  5. snowflake/cli/api/commands/flags.py +45 -36
  6. snowflake/cli/api/commands/project_initialisation.py +5 -2
  7. snowflake/cli/api/commands/snow_typer.py +20 -9
  8. snowflake/cli/api/errno.py +27 -0
  9. snowflake/cli/api/feature_flags.py +1 -0
  10. snowflake/cli/api/identifiers.py +20 -3
  11. snowflake/cli/api/output/types.py +9 -0
  12. snowflake/cli/api/project/definition_manager.py +2 -2
  13. snowflake/cli/api/project/project_verification.py +23 -0
  14. snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
  15. snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
  16. snowflake/cli/api/project/schemas/entities/common.py +85 -0
  17. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  18. snowflake/cli/api/project/schemas/project_definition.py +114 -22
  19. snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
  20. snowflake/cli/api/project/schemas/template.py +77 -0
  21. snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
  22. snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
  23. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  24. snowflake/cli/api/rendering/project_templates.py +97 -0
  25. snowflake/cli/api/rendering/sql_templates.py +56 -0
  26. snowflake/cli/api/sql_execution.py +40 -1
  27. snowflake/cli/api/utils/definition_rendering.py +8 -5
  28. snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
  29. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
  30. snowflake/cli/app/loggers.py +3 -1
  31. snowflake/cli/app/printing.py +17 -7
  32. snowflake/cli/app/snow_connector.py +9 -1
  33. snowflake/cli/app/telemetry.py +41 -2
  34. snowflake/cli/plugins/connection/commands.py +4 -3
  35. snowflake/cli/plugins/connection/util.py +73 -18
  36. snowflake/cli/plugins/cortex/commands.py +2 -1
  37. snowflake/cli/plugins/git/commands.py +20 -4
  38. snowflake/cli/plugins/git/manager.py +44 -20
  39. snowflake/cli/plugins/init/__init__.py +13 -0
  40. snowflake/cli/plugins/init/commands.py +242 -0
  41. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  42. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
  43. snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
  44. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
  45. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  46. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  47. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
  48. snowflake/cli/plugins/nativeapp/commands.py +69 -6
  49. snowflake/cli/plugins/nativeapp/constants.py +0 -6
  50. snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
  51. snowflake/cli/plugins/nativeapp/init.py +1 -1
  52. snowflake/cli/plugins/nativeapp/manager.py +114 -39
  53. snowflake/cli/plugins/nativeapp/project_model.py +8 -4
  54. snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
  55. snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
  56. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
  57. snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
  58. snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
  59. snowflake/cli/plugins/snowpark/commands.py +34 -26
  60. snowflake/cli/plugins/snowpark/common.py +88 -27
  61. snowflake/cli/plugins/snowpark/manager.py +16 -5
  62. snowflake/cli/plugins/snowpark/models.py +6 -0
  63. snowflake/cli/plugins/sql/commands.py +3 -5
  64. snowflake/cli/plugins/sql/manager.py +1 -1
  65. snowflake/cli/plugins/stage/commands.py +2 -2
  66. snowflake/cli/plugins/stage/diff.py +4 -2
  67. snowflake/cli/plugins/stage/manager.py +290 -86
  68. snowflake/cli/plugins/streamlit/commands.py +20 -6
  69. snowflake/cli/plugins/streamlit/manager.py +29 -27
  70. snowflake/cli/plugins/workspace/__init__.py +13 -0
  71. snowflake/cli/plugins/workspace/commands.py +35 -0
  72. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  73. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
  74. snowflake/cli/templates/default_snowpark/app/common.py +0 -15
  75. snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
  76. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
  77. snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
  78. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
  79. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
  80. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/METADATA +7 -6
  81. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/RECORD +84 -64
  82. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/WHEEL +0 -0
  83. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc1.dist-info}/entry_points.txt +0 -0
  84. {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 Any, Dict, Optional, Union
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 _BaseDefinition(UpdatableModel):
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
- if version not in _version_map:
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(_version_map)}'
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 _DefinitionV10(_BaseDefinition):
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 _DefinitionV11(_DefinitionV10):
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 ProjectDefinition(_DefinitionV11):
106
- def __init__(self, **kwargs):
107
- super().__init__(**kwargs)
108
- self._validate(kwargs)
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
- @staticmethod
111
- def _validate(data: Any):
112
- if not isinstance(data, dict):
113
- return
114
- if version := str(data.get("definition_version")):
115
- version_model = _version_map.get(version)
116
- if not version_model:
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"Unknown schema version: {version}. Supported version: {_supported_version}"
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
- _version_map = {"1": _DefinitionV10, "1.1": _DefinitionV11}
124
- _supported_version = tuple(_version_map.keys())
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[str] = Field(
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[str] = Field(
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[str] = Field(title="Streamlit pages", default=None)
39
- additional_source_files: Optional[List[str]] = Field(
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
+ )
@@ -11,5 +11,3 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
15
- APPLICATION_NO_LONGER_AVAILABLE = 93079
@@ -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 _env_bootstrap(env: Environment) -> Environment:
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 = _env_bootstrap(
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)