snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc2__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 (66) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/snow_connector.py +91 -37
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/connection/commands.py +3 -2
  7. snowflake/cli/_plugins/git/commands.py +55 -14
  8. snowflake/cli/_plugins/git/manager.py +14 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  10. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  11. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  12. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
  13. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
  14. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  15. snowflake/cli/_plugins/nativeapp/manager.py +74 -144
  16. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  17. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  18. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  19. snowflake/cli/_plugins/nativeapp/teardown_processor.py +17 -246
  20. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  21. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  22. snowflake/cli/_plugins/snowpark/common.py +17 -1
  23. snowflake/cli/_plugins/snowpark/models.py +2 -1
  24. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  25. snowflake/cli/_plugins/sql/commands.py +1 -2
  26. snowflake/cli/_plugins/stage/commands.py +2 -2
  27. snowflake/cli/_plugins/stage/manager.py +46 -15
  28. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  29. snowflake/cli/_plugins/streamlit/manager.py +13 -0
  30. snowflake/cli/_plugins/workspace/action_context.py +7 -0
  31. snowflake/cli/_plugins/workspace/commands.py +145 -32
  32. snowflake/cli/_plugins/workspace/manager.py +21 -4
  33. snowflake/cli/api/cli_global_context.py +136 -313
  34. snowflake/cli/api/commands/decorators.py +1 -1
  35. snowflake/cli/api/commands/flags.py +106 -102
  36. snowflake/cli/api/commands/snow_typer.py +15 -6
  37. snowflake/cli/api/config.py +18 -5
  38. snowflake/cli/api/connections.py +214 -0
  39. snowflake/cli/api/console/abc.py +4 -2
  40. snowflake/cli/api/constants.py +11 -0
  41. snowflake/cli/api/entities/application_entity.py +687 -2
  42. snowflake/cli/api/entities/application_package_entity.py +407 -9
  43. snowflake/cli/api/entities/common.py +7 -2
  44. snowflake/cli/api/entities/utils.py +80 -20
  45. snowflake/cli/api/exceptions.py +12 -2
  46. snowflake/cli/api/feature_flags.py +0 -2
  47. snowflake/cli/api/identifiers.py +3 -0
  48. snowflake/cli/api/project/definition.py +35 -1
  49. snowflake/cli/api/project/definition_conversion.py +352 -0
  50. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  51. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  52. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  53. snowflake/cli/api/project/schemas/project_definition.py +102 -43
  54. snowflake/cli/api/rendering/jinja.py +2 -16
  55. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  56. snowflake/cli/api/rendering/sql_templates.py +14 -4
  57. snowflake/cli/api/secure_path.py +13 -18
  58. snowflake/cli/api/secure_utils.py +90 -1
  59. snowflake/cli/api/sql_execution.py +13 -0
  60. snowflake/cli/api/utils/definition_rendering.py +7 -7
  61. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
  62. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
  63. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  64. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  65. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  66. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -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,
@@ -327,11 +323,8 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
327
323
  predicate=is_python_file_artifact,
328
324
  )
329
325
  ):
330
- cc.step(
331
- "Processing Snowpark annotations from {}".format(
332
- dest_file.relative_to(bundle_map.deploy_root())
333
- )
334
- )
326
+ src_file_name = src_file.relative_to(self._bundle_ctx.project_root)
327
+ cc.step(f"Processing Snowpark annotations from {src_file_name}")
335
328
  collected_extension_function_json = _execute_in_sandbox(
336
329
  py_file=str(dest_file.resolve()),
337
330
  deploy_root=self._bundle_ctx.deploy_root,
@@ -366,7 +359,9 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
366
359
  Generates a SQL filename for the generated root from the python file, and creates its parent directories.
367
360
  """
368
361
  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"))
362
+ sql_file = Path(
363
+ self._bundle_ctx.generated_root, relative_py_file.with_suffix(".sql")
364
+ )
370
365
  if sql_file.exists():
371
366
  cc.warning(
372
367
  f"""\
@@ -0,0 +1,111 @@
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 pathlib import Path
18
+ from typing import Optional
19
+
20
+ import jinja2
21
+ from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
22
+ from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import (
23
+ ArtifactProcessor,
24
+ )
25
+ from snowflake.cli._plugins.nativeapp.exceptions import InvalidTemplateInFileError
26
+ from snowflake.cli.api.cli_global_context import get_cli_context
27
+ from snowflake.cli.api.console import cli_console as cc
28
+ from snowflake.cli.api.project.schemas.native_app.path_mapping import (
29
+ PathMapping,
30
+ ProcessorMapping,
31
+ )
32
+ from snowflake.cli.api.rendering.project_definition_templates import (
33
+ get_client_side_jinja_env,
34
+ has_client_side_templates,
35
+ )
36
+ from snowflake.cli.api.rendering.sql_templates import (
37
+ choose_sql_jinja_env_based_on_template_syntax,
38
+ has_sql_templates,
39
+ )
40
+
41
+
42
+ def _is_sql_file(file: Path) -> bool:
43
+ return file.name.lower().endswith(".sql")
44
+
45
+
46
+ class TemplatesProcessor(ArtifactProcessor):
47
+ """
48
+ Processor class to perform template expansion on all relevant artifacts (specified in the project definition file).
49
+ """
50
+
51
+ def expand_templates_in_file(self, src: Path, dest: Path) -> None:
52
+ """
53
+ Expand templates in the file.
54
+ """
55
+ if src.is_dir():
56
+ return
57
+
58
+ with self.edit_file(dest) as file:
59
+ if not has_client_side_templates(file.contents) and not (
60
+ _is_sql_file(dest) and has_sql_templates(file.contents)
61
+ ):
62
+ return
63
+
64
+ src_file_name = src.relative_to(self._bundle_ctx.project_root)
65
+ cc.step(f"Expanding templates in {src_file_name}")
66
+ with cc.indented():
67
+ try:
68
+ jinja_env = (
69
+ choose_sql_jinja_env_based_on_template_syntax(
70
+ file.contents, reference_name=src_file_name
71
+ )
72
+ if _is_sql_file(dest)
73
+ else get_client_side_jinja_env()
74
+ )
75
+ expanded_template = jinja_env.from_string(file.contents).render(
76
+ get_cli_context().template_context
77
+ )
78
+
79
+ # For now, we are printing the source file path in the error message
80
+ # instead of the destination file path to make it easier for the user
81
+ # to identify the file that has the error, and edit the correct file.
82
+ except jinja2.TemplateSyntaxError as e:
83
+ raise InvalidTemplateInFileError(src_file_name, e, e.lineno) from e
84
+
85
+ except jinja2.UndefinedError as e:
86
+ raise InvalidTemplateInFileError(src_file_name, e) from e
87
+
88
+ if expanded_template != file.contents:
89
+ file.edited_contents = expanded_template
90
+
91
+ def process(
92
+ self,
93
+ artifact_to_process: PathMapping,
94
+ processor_mapping: Optional[ProcessorMapping],
95
+ **kwargs,
96
+ ) -> None:
97
+ """
98
+ Process the artifact by executing the template expansion logic on it.
99
+ """
100
+
101
+ bundle_map = BundleMap(
102
+ project_root=self._bundle_ctx.project_root,
103
+ deploy_root=self._bundle_ctx.deploy_root,
104
+ )
105
+ bundle_map.add(artifact_to_process)
106
+
107
+ for src, dest in bundle_map.all_mappings(
108
+ absolute=True,
109
+ expand_directories=True,
110
+ ):
111
+ self.expand_templates_in_file(src, dest)
@@ -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