snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/snow_connector.py +18 -11
  3. snowflake/cli/_plugins/connection/commands.py +3 -2
  4. snowflake/cli/_plugins/git/manager.py +14 -6
  5. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  6. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  7. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  8. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
  10. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  11. snowflake/cli/_plugins/nativeapp/manager.py +29 -58
  12. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  13. snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
  14. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  15. snowflake/cli/_plugins/snowpark/common.py +17 -1
  16. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  17. snowflake/cli/_plugins/sql/commands.py +1 -2
  18. snowflake/cli/_plugins/stage/commands.py +2 -2
  19. snowflake/cli/_plugins/stage/manager.py +46 -15
  20. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  21. snowflake/cli/_plugins/streamlit/manager.py +4 -0
  22. snowflake/cli/_plugins/workspace/action_context.py +6 -0
  23. snowflake/cli/_plugins/workspace/commands.py +103 -22
  24. snowflake/cli/_plugins/workspace/manager.py +20 -4
  25. snowflake/cli/api/cli_global_context.py +6 -6
  26. snowflake/cli/api/commands/decorators.py +1 -1
  27. snowflake/cli/api/commands/flags.py +31 -12
  28. snowflake/cli/api/commands/snow_typer.py +9 -2
  29. snowflake/cli/api/config.py +17 -4
  30. snowflake/cli/api/constants.py +11 -0
  31. snowflake/cli/api/entities/application_package_entity.py +296 -3
  32. snowflake/cli/api/entities/common.py +6 -2
  33. snowflake/cli/api/entities/utils.py +46 -10
  34. snowflake/cli/api/exceptions.py +12 -2
  35. snowflake/cli/api/feature_flags.py +0 -2
  36. snowflake/cli/api/project/definition.py +24 -1
  37. snowflake/cli/api/project/definition_conversion.py +194 -0
  38. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  39. snowflake/cli/api/project/schemas/project_definition.py +1 -4
  40. snowflake/cli/api/rendering/jinja.py +2 -16
  41. snowflake/cli/api/rendering/project_definition_templates.py +1 -1
  42. snowflake/cli/api/rendering/sql_templates.py +7 -4
  43. snowflake/cli/api/secure_path.py +13 -18
  44. snowflake/cli/api/secure_utils.py +90 -1
  45. snowflake/cli/api/sql_execution.py +13 -0
  46. snowflake/cli/api/utils/definition_rendering.py +4 -6
  47. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
  48. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
  49. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
  50. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  51. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -14,4 +14,4 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- VERSION = "3.0.0rc0"
17
+ VERSION = "3.0.0rc1"
@@ -71,6 +71,7 @@ def connect_to_snowflake(
71
71
 
72
72
  if connection_name:
73
73
  connection_parameters = get_connection_dict(connection_name)
74
+ connection_parameters = get_connection_dict(connection_name)
74
75
  elif temporary_connection:
75
76
  connection_parameters = {} # we will apply overrides in next step
76
77
  else:
@@ -164,18 +165,24 @@ def _raise_errors_related_to_session_token(
164
165
 
165
166
 
166
167
  def update_connection_details_with_private_key(connection_parameters: Dict):
167
- if "private_key_path" in connection_parameters:
168
- if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
169
- private_key = _load_pem_to_der(connection_parameters["private_key_path"])
170
- connection_parameters["private_key"] = private_key
171
- del connection_parameters["private_key_path"]
172
- else:
173
- raise ClickException(
174
- "Private Key authentication requires authenticator set to SNOWFLAKE_JWT"
175
- )
168
+ if "private_key_file" in connection_parameters:
169
+ _load_private_key(connection_parameters, "private_key_file")
170
+ elif "private_key_path" in connection_parameters:
171
+ _load_private_key(connection_parameters, "private_key_path")
176
172
  return connection_parameters
177
173
 
178
174
 
175
+ def _load_private_key(connection_parameters: Dict, private_key_var_name: str) -> None:
176
+ if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
177
+ private_key = _load_pem_to_der(connection_parameters[private_key_var_name])
178
+ connection_parameters["private_key"] = private_key
179
+ del connection_parameters[private_key_var_name]
180
+ else:
181
+ raise ClickException(
182
+ "Private Key authentication requires authenticator set to SNOWFLAKE_JWT"
183
+ )
184
+
185
+
179
186
  def _update_connection_application_name(connection_parameters: Dict):
180
187
  """Update version and name of app handling connection."""
181
188
  connection_application_params = {
@@ -184,13 +191,13 @@ def _update_connection_application_name(connection_parameters: Dict):
184
191
  connection_parameters.update(connection_application_params)
185
192
 
186
193
 
187
- def _load_pem_to_der(private_key_path: str) -> bytes:
194
+ def _load_pem_to_der(private_key_file: str) -> bytes:
188
195
  """
189
196
  Given a private key file path (in PEM format), decode key data into DER
190
197
  format
191
198
  """
192
199
 
193
- with SecurePath(private_key_path).open(
200
+ with SecurePath(private_key_file).open(
194
201
  "rb", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
195
202
  ) as f:
196
203
  private_key_pem = f.read()
@@ -224,9 +224,10 @@ def add(
224
224
  prompt="Authentication method",
225
225
  help="Chosen authenticator, if other than password-based",
226
226
  ),
227
- private_key_path: str = typer.Option(
227
+ private_key_file: str = typer.Option(
228
228
  EmptyInput(),
229
229
  "--private-key",
230
+ "--private-key-path",
230
231
  "-k",
231
232
  click_type=OptionalPrompt(),
232
233
  prompt="Path to private key file",
@@ -268,7 +269,7 @@ def add(
268
269
  warehouse=warehouse,
269
270
  role=role,
270
271
  authenticator=authenticator,
271
- private_key_path=private_key_path,
272
+ private_key_file=private_key_file,
272
273
  token_file_path=token_file_path,
273
274
  ),
274
275
  )
@@ -41,17 +41,25 @@ class GitStagePathParts(StagePathParts):
41
41
 
42
42
  @property
43
43
  def path(self) -> str:
44
- return (
45
- f"{self.stage_name}{self.directory}"
46
- if self.stage_name.endswith("/")
47
- else f"{self.stage_name}/{self.directory}"
48
- )
44
+ return f"{self.stage_name.rstrip('/')}/{self.directory}"
49
45
 
50
- def add_stage_prefix(self, file_path: str) -> str:
46
+ @classmethod
47
+ def get_directory(cls, stage_path: str) -> str:
48
+ return "/".join(Path(stage_path).parts[3:])
49
+
50
+ @property
51
+ def full_path(self) -> str:
52
+ return f"{self.stage.rstrip('/')}/{self.directory}"
53
+
54
+ def replace_stage_prefix(self, file_path: str) -> str:
51
55
  stage = Path(self.stage).parts[0]
52
56
  file_path_without_prefix = Path(file_path).parts[1:]
53
57
  return f"{stage}/{'/'.join(file_path_without_prefix)}"
54
58
 
59
+ def add_stage_prefix(self, file_path: str) -> str:
60
+ stage = self.stage.rstrip("/")
61
+ return f"{stage}/{file_path.lstrip('/')}"
62
+
55
63
  def get_directory_from_file_path(self, file_path: str) -> List[str]:
56
64
  stage_path_length = len(Path(self.directory).parts)
57
65
  return list(Path(file_path).parts[3 + stage_path_length : -1])
@@ -14,8 +14,11 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import copy
18
+ import re
17
19
  from typing import Dict, Optional
18
20
 
21
+ from click import ClickException
19
22
  from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
20
23
  from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import (
21
24
  ArtifactProcessor,
@@ -27,6 +30,9 @@ from snowflake.cli._plugins.nativeapp.codegen.setup.native_app_setup_processor i
27
30
  from snowflake.cli._plugins.nativeapp.codegen.snowpark.python_processor import (
28
31
  SnowparkAnnotationProcessor,
29
32
  )
33
+ from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import (
34
+ TemplatesProcessor,
35
+ )
30
36
  from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
31
37
  from snowflake.cli.api.console import cli_console as cc
32
38
  from snowflake.cli.api.project.schemas.native_app.path_mapping import (
@@ -34,11 +40,13 @@ from snowflake.cli.api.project.schemas.native_app.path_mapping import (
34
40
  )
35
41
 
36
42
  SNOWPARK_PROCESSOR = "snowpark"
37
- NA_SETUP_PROCESSOR = "native-app-setup"
43
+ NA_SETUP_PROCESSOR = "native app setup"
44
+ TEMPLATES_PROCESSOR = "templates"
38
45
 
39
46
  _REGISTERED_PROCESSORS_BY_NAME = {
40
47
  SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor,
41
48
  NA_SETUP_PROCESSOR: NativeAppSetupProcessor,
49
+ TEMPLATES_PROCESSOR: TemplatesProcessor,
42
50
  }
43
51
 
44
52
 
@@ -110,7 +118,15 @@ class NativeAppCompiler:
110
118
  # No registered processor with the specified name
111
119
  return None
112
120
 
113
- current_processor = processor_factory(self._bundle_ctx)
121
+ processor_ctx = copy.copy(self._bundle_ctx)
122
+ processor_subdirectory = re.sub(r"[^a-zA-Z0-9_$]", "_", processor_name)
123
+ processor_ctx.bundle_root = (
124
+ self._bundle_ctx.bundle_root / processor_subdirectory
125
+ )
126
+ processor_ctx.generated_root = (
127
+ self._bundle_ctx.generated_root / processor_subdirectory
128
+ )
129
+ current_processor = processor_factory(processor_ctx)
114
130
  self.cached_processors[processor_name] = current_processor
115
131
 
116
132
  return current_processor
@@ -15,12 +15,18 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
+ import logging
18
19
  import os.path
19
20
  from pathlib import Path
20
21
  from typing import List, Optional
21
22
 
23
+ import yaml
22
24
  from click import ClickException
23
- from snowflake.cli._plugins.nativeapp.artifacts import BundleMap, find_setup_script_file
25
+ from snowflake.cli._plugins.nativeapp.artifacts import (
26
+ BundleMap,
27
+ find_manifest_file,
28
+ find_setup_script_file,
29
+ )
24
30
  from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import (
25
31
  ArtifactProcessor,
26
32
  is_python_file_artifact,
@@ -40,6 +46,32 @@ from snowflake.cli.api.project.schemas.native_app.path_mapping import (
40
46
  DEFAULT_TIMEOUT = 30
41
47
  DRIVER_PATH = Path(__file__).parent / "setup_driver.py.source"
42
48
 
49
+ log = logging.getLogger(__name__)
50
+
51
+
52
+ def safe_set(d: dict, *keys: str, **kwargs) -> None:
53
+ """
54
+ Sets a value in a nested dictionary structure, creating intermediate dictionaries as needed.
55
+ Sample usage:
56
+
57
+ d = {}
58
+ safe_set(d, "a", "b", "c", value=42)
59
+
60
+ d is now:
61
+ {
62
+ "a": {
63
+ "b": {
64
+ "c": 42
65
+ }
66
+ }
67
+ }
68
+ """
69
+ curr = d
70
+ for k in keys[:-1]:
71
+ curr = curr.setdefault(k, {})
72
+
73
+ curr[keys[-1]] = kwargs.get("value")
74
+
43
75
 
44
76
  class NativeAppSetupProcessor(ArtifactProcessor):
45
77
  def __init__(self, *args, **kwargs):
@@ -62,7 +94,7 @@ class NativeAppSetupProcessor(ArtifactProcessor):
62
94
 
63
95
  self._create_or_update_sandbox()
64
96
 
65
- cc.phase("Processing Python setup files")
97
+ cc.step("Processing Python setup files")
66
98
 
67
99
  files_to_process = []
68
100
  for src_file, dest_file in bundle_map.all_mappings(
@@ -73,18 +105,55 @@ class NativeAppSetupProcessor(ArtifactProcessor):
73
105
  )
74
106
  files_to_process.append(src_file)
75
107
 
76
- sql_files_mapping = self._execute_in_sandbox(files_to_process)
77
- self._generate_setup_sql(sql_files_mapping)
108
+ result = self._execute_in_sandbox(files_to_process)
109
+ if not result:
110
+ return # nothing to do
111
+
112
+ logs = result.get("logs", [])
113
+ for msg in logs:
114
+ log.debug(msg)
115
+
116
+ warnings = result.get("warnings", [])
117
+ for msg in warnings:
118
+ cc.warning(msg)
119
+
120
+ schema_version = result.get("schema_version")
121
+ if schema_version != "1":
122
+ raise ClickException(
123
+ f"Unsupported schema version returned from snowflake-app-python library: {schema_version}"
124
+ )
125
+
126
+ setup_script_mods = [
127
+ mod
128
+ for mod in result.get("modifications", [])
129
+ if mod.get("target") == "native_app:setup_script"
130
+ ]
131
+ if setup_script_mods:
132
+ self._edit_setup_sql(setup_script_mods)
133
+
134
+ manifest_mods = [
135
+ mod
136
+ for mod in result.get("modifications", [])
137
+ if mod.get("target") == "native_app:manifest"
138
+ ]
139
+ if manifest_mods:
140
+ self._edit_manifest(manifest_mods)
78
141
 
79
142
  def _execute_in_sandbox(self, py_files: List[Path]) -> dict:
80
143
  file_count = len(py_files)
81
144
  cc.step(f"Processing {file_count} setup file{'s' if file_count > 1 else ''}")
82
145
 
146
+ manifest_path = find_manifest_file(deploy_root=self._bundle_ctx.deploy_root)
147
+
148
+ generated_root = self._bundle_ctx.generated_root
149
+ generated_root.mkdir(exist_ok=True, parents=True)
150
+
83
151
  env_vars = {
84
152
  "_SNOWFLAKE_CLI_PROJECT_PATH": str(self._bundle_ctx.project_root),
85
153
  "_SNOWFLAKE_CLI_SETUP_FILES": os.pathsep.join(map(str, py_files)),
86
154
  "_SNOWFLAKE_CLI_APP_NAME": str(self._bundle_ctx.package_name),
87
- "_SNOWFLAKE_CLI_SQL_DEST_DIR": str(self.generated_root),
155
+ "_SNOWFLAKE_CLI_SQL_DEST_DIR": str(generated_root),
156
+ "_SNOWFLAKE_CLI_MANIFEST_PATH": str(manifest_path),
88
157
  }
89
158
 
90
159
  try:
@@ -102,56 +171,68 @@ class NativeAppSetupProcessor(ArtifactProcessor):
102
171
  )
103
172
 
104
173
  if result.returncode == 0:
105
- sql_file_mappings = json.loads(result.stdout)
106
- return sql_file_mappings
174
+ return json.loads(result.stdout)
107
175
  else:
108
176
  raise ClickException(
109
177
  f"Failed to execute python setup script logic: {result.stderr}"
110
178
  )
111
179
 
112
- def _generate_setup_sql(self, sql_file_mappings: dict) -> None:
113
- if not sql_file_mappings:
114
- # Nothing to generate
115
- return
116
-
117
- generated_root = self.generated_root
118
- generated_root.mkdir(exist_ok=True, parents=True)
119
-
180
+ def _edit_setup_sql(self, modifications: List[dict]) -> None:
120
181
  cc.step("Patching setup script")
121
182
  setup_file_path = find_setup_script_file(
122
183
  deploy_root=self._bundle_ctx.deploy_root
123
184
  )
124
- with self.edit_file(setup_file_path) as f:
125
- new_contents = [f.contents]
126
185
 
127
- if sql_file_mappings["schemas"]:
128
- schemas_file = generated_root / sql_file_mappings["schemas"]
129
- new_contents.insert(
130
- 0,
131
- f"EXECUTE IMMEDIATE FROM '/{to_stage_path(schemas_file.relative_to(self._bundle_ctx.deploy_root))}';",
132
- )
133
-
134
- if sql_file_mappings["compute_pools"]:
135
- compute_pools_file = generated_root / sql_file_mappings["compute_pools"]
136
- new_contents.append(
137
- f"EXECUTE IMMEDIATE FROM '/{to_stage_path(compute_pools_file.relative_to(self._bundle_ctx.deploy_root))}';"
138
- )
139
-
140
- if sql_file_mappings["services"]:
141
- services_file = generated_root / sql_file_mappings["services"]
142
- new_contents.append(
143
- f"EXECUTE IMMEDIATE FROM '/{to_stage_path(services_file.relative_to(self._bundle_ctx.deploy_root))}';"
144
- )
145
-
146
- f.edited_contents = "\n".join(new_contents)
186
+ with self.edit_file(setup_file_path) as f:
187
+ prepended = []
188
+ appended = []
189
+
190
+ for mod in modifications:
191
+ for inst in mod.get("instructions", []):
192
+ if inst.get("type") == "insert":
193
+ default_loc = inst.get("default_location")
194
+ if default_loc == "end":
195
+ appended.append(self._setup_mod_instruction_to_sql(inst))
196
+ elif default_loc == "start":
197
+ prepended.append(self._setup_mod_instruction_to_sql(inst))
198
+
199
+ if prepended or appended:
200
+ f.edited_contents = "\n".join(prepended + [f.contents] + appended)
201
+
202
+ def _edit_manifest(self, modifications: List[dict]) -> None:
203
+ cc.step("Patching manifest")
204
+ manifest_path = find_manifest_file(deploy_root=self._bundle_ctx.deploy_root)
205
+
206
+ with self.edit_file(manifest_path) as f:
207
+ manifest = yaml.safe_load(f.contents)
208
+
209
+ for mod in modifications:
210
+ for inst in mod.get("instructions", []):
211
+ if inst.get("type") == "set":
212
+ payload = inst.get("payload")
213
+ if payload:
214
+ key = payload.get("key")
215
+ value = payload.get("value")
216
+ safe_set(manifest, *key.split("."), value=value)
217
+ f.edited_contents = yaml.safe_dump(manifest, sort_keys=False)
218
+
219
+ def _setup_mod_instruction_to_sql(self, mod_inst: dict) -> str:
220
+ payload = mod_inst.get("payload")
221
+ if not payload:
222
+ raise ClickException("Unsupported instruction received: no payload found")
223
+
224
+ payload_type = payload.get("type")
225
+ if payload_type == "execute immediate":
226
+ file_path = payload.get("file_path")
227
+ if file_path:
228
+ sql_file_path = self._bundle_ctx.generated_root / file_path
229
+ return f"EXECUTE IMMEDIATE FROM '/{to_stage_path(sql_file_path.relative_to(self._bundle_ctx.deploy_root))}';"
230
+
231
+ raise ClickException(f"Unsupported instruction type received: {payload_type}")
147
232
 
148
233
  @property
149
234
  def sandbox_root(self):
150
- return self._bundle_ctx.bundle_root / "setup_py_venv"
151
-
152
- @property
153
- def generated_root(self):
154
- return self._bundle_ctx.generated_root / "setup_py"
235
+ return self._bundle_ctx.bundle_root / "venv"
155
236
 
156
237
  def _create_or_update_sandbox(self):
157
238
  sandbox_root = self.sandbox_root
@@ -20,8 +20,11 @@ from pathlib import Path
20
20
  import snowflake.app.context as ctx
21
21
  from snowflake.app.sql import SQLGenerator
22
22
 
23
- ctx._project_path = os.environ["_SNOWFLAKE_CLI_PROJECT_PATH"]
24
- ctx._current_app_name = os.environ["_SNOWFLAKE_CLI_APP_NAME"]
23
+ ctx.configure("project_path", os.environ.get("_SNOWFLAKE_CLI_PROJECT_PATH", None))
24
+ ctx.configure("manifest_path", os.environ.get("_SNOWFLAKE_CLI_MANIFEST_PATH", None))
25
+ ctx.configure("current_app_name", os.environ.get("_SNOWFLAKE_CLI_APP_NAME", None))
26
+ ctx.configure("enable_sql_generation", True)
27
+
25
28
  __snowflake_internal_py_files = os.environ["_SNOWFLAKE_CLI_SETUP_FILES"].split(
26
29
  os.pathsep
27
30
  )
@@ -226,13 +226,9 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
226
226
  edit_setup_script_with_exec_imm_sql(
227
227
  collected_sql_files=collected_sql_files,
228
228
  deploy_root=bundle_map.deploy_root(),
229
- generated_root=self._generated_root,
229
+ generated_root=self._bundle_ctx.generated_root,
230
230
  )
231
231
 
232
- @property
233
- def _generated_root(self):
234
- return self._bundle_ctx.generated_root / "snowpark"
235
-
236
232
  def _normalize_imports(
237
233
  self,
238
234
  extension_fn: NativeAppExtensionFunction,
@@ -366,7 +362,9 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
366
362
  Generates a SQL filename for the generated root from the python file, and creates its parent directories.
367
363
  """
368
364
  relative_py_file = py_file.relative_to(self._bundle_ctx.deploy_root)
369
- sql_file = Path(self._generated_root, relative_py_file.with_suffix(".sql"))
365
+ sql_file = Path(
366
+ self._bundle_ctx.generated_root, relative_py_file.with_suffix(".sql")
367
+ )
370
368
  if sql_file.exists():
371
369
  cc.warning(
372
370
  f"""\
@@ -0,0 +1,93 @@
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 Optional
18
+
19
+ import jinja2
20
+ from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
21
+ from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import (
22
+ ArtifactProcessor,
23
+ )
24
+ from snowflake.cli._plugins.nativeapp.exceptions import InvalidTemplateInFileError
25
+ from snowflake.cli.api.cli_global_context import get_cli_context
26
+ from snowflake.cli.api.console import cli_console as cc
27
+ from snowflake.cli.api.project.schemas.native_app.path_mapping import (
28
+ PathMapping,
29
+ ProcessorMapping,
30
+ )
31
+ from snowflake.cli.api.rendering.project_definition_templates import (
32
+ get_client_side_jinja_env,
33
+ )
34
+ from snowflake.cli.api.rendering.sql_templates import (
35
+ choose_sql_jinja_env_based_on_template_syntax,
36
+ )
37
+
38
+
39
+ class TemplatesProcessor(ArtifactProcessor):
40
+ """
41
+ Processor class to perform template expansion on all relevant artifacts (specified in the project definition file).
42
+ """
43
+
44
+ def process(
45
+ self,
46
+ artifact_to_process: PathMapping,
47
+ processor_mapping: Optional[ProcessorMapping],
48
+ **kwargs,
49
+ ):
50
+ """
51
+ Process the artifact by executing the template expansion logic on it.
52
+ """
53
+ cc.step(f"Processing artifact {artifact_to_process} with templates processor")
54
+
55
+ bundle_map = BundleMap(
56
+ project_root=self._bundle_ctx.project_root,
57
+ deploy_root=self._bundle_ctx.deploy_root,
58
+ )
59
+ bundle_map.add(artifact_to_process)
60
+
61
+ for src, dest in bundle_map.all_mappings(
62
+ absolute=True,
63
+ expand_directories=True,
64
+ ):
65
+ if src.is_dir():
66
+ continue
67
+ with self.edit_file(dest) as f:
68
+ file_name = src.relative_to(self._bundle_ctx.project_root)
69
+
70
+ jinja_env = (
71
+ choose_sql_jinja_env_based_on_template_syntax(
72
+ f.contents, reference_name=file_name
73
+ )
74
+ if dest.name.lower().endswith(".sql")
75
+ else get_client_side_jinja_env()
76
+ )
77
+
78
+ try:
79
+ expanded_template = jinja_env.from_string(f.contents).render(
80
+ get_cli_context().template_context
81
+ )
82
+
83
+ # For now, we are printing the source file path in the error message
84
+ # instead of the destination file path to make it easier for the user
85
+ # to identify the file that has the error, and edit the correct file.
86
+ except jinja2.TemplateSyntaxError as e:
87
+ raise InvalidTemplateInFileError(file_name, e, e.lineno) from e
88
+
89
+ except jinja2.UndefinedError as e:
90
+ raise InvalidTemplateInFileError(file_name, e) from e
91
+
92
+ if expanded_template != f.contents:
93
+ f.edited_contents = expanded_template
@@ -64,15 +64,15 @@ class MissingScriptError(ClickException):
64
64
  super().__init__(f'Script "{relpath}" does not exist')
65
65
 
66
66
 
67
- class InvalidScriptError(ClickException):
68
- """A referenced script had syntax error(s)."""
67
+ class InvalidTemplateInFileError(ClickException):
68
+ """A referenced templated file had syntax error(s)."""
69
69
 
70
70
  def __init__(
71
71
  self, relpath: str, err: jinja2.TemplateError, lineno: Optional[int] = None
72
72
  ):
73
73
  lineno_str = f":{lineno}" if lineno is not None else ""
74
74
  super().__init__(
75
- f'Script "{relpath}{lineno_str}" does not contain a valid template: {err.message}'
75
+ f'File "{relpath}{lineno_str}" does not contain a valid template: {err.message}'
76
76
  )
77
77
  self.err = err
78
78