snowflake-cli-labs 2.6.0rc0__py3-none-any.whl → 2.7.0rc0__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 (89) 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 +4 -1
  7. snowflake/cli/api/commands/snow_typer.py +20 -9
  8. snowflake/cli/api/config.py +3 -0
  9. snowflake/cli/api/errno.py +27 -0
  10. snowflake/cli/api/feature_flags.py +1 -0
  11. snowflake/cli/api/identifiers.py +20 -3
  12. snowflake/cli/api/output/types.py +9 -0
  13. snowflake/cli/api/project/definition_manager.py +2 -2
  14. snowflake/cli/api/project/project_verification.py +23 -0
  15. snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
  16. snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
  17. snowflake/cli/api/project/schemas/entities/common.py +85 -0
  18. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  19. snowflake/cli/api/project/schemas/project_definition.py +114 -22
  20. snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
  21. snowflake/cli/api/project/schemas/template.py +77 -0
  22. snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
  23. snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
  24. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  25. snowflake/cli/api/rendering/project_templates.py +97 -0
  26. snowflake/cli/api/rendering/sql_templates.py +56 -0
  27. snowflake/cli/api/rest_api.py +84 -25
  28. snowflake/cli/api/sql_execution.py +40 -1
  29. snowflake/cli/api/utils/definition_rendering.py +8 -5
  30. snowflake/cli/app/cli_app.py +0 -2
  31. snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
  32. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
  33. snowflake/cli/app/loggers.py +10 -6
  34. snowflake/cli/app/printing.py +17 -7
  35. snowflake/cli/app/snow_connector.py +9 -1
  36. snowflake/cli/app/telemetry.py +41 -2
  37. snowflake/cli/plugins/connection/commands.py +4 -3
  38. snowflake/cli/plugins/connection/util.py +73 -18
  39. snowflake/cli/plugins/cortex/commands.py +2 -1
  40. snowflake/cli/plugins/git/commands.py +20 -4
  41. snowflake/cli/plugins/git/manager.py +44 -20
  42. snowflake/cli/plugins/init/__init__.py +13 -0
  43. snowflake/cli/plugins/init/commands.py +242 -0
  44. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  45. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
  46. snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
  47. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
  48. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  49. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  50. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
  51. snowflake/cli/plugins/nativeapp/commands.py +69 -6
  52. snowflake/cli/plugins/nativeapp/constants.py +0 -6
  53. snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
  54. snowflake/cli/plugins/nativeapp/init.py +1 -1
  55. snowflake/cli/plugins/nativeapp/manager.py +114 -39
  56. snowflake/cli/plugins/nativeapp/project_model.py +8 -4
  57. snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
  58. snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
  59. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
  60. snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
  61. snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
  62. snowflake/cli/plugins/object/commands.py +1 -1
  63. snowflake/cli/plugins/object/manager.py +2 -15
  64. snowflake/cli/plugins/snowpark/commands.py +34 -26
  65. snowflake/cli/plugins/snowpark/common.py +88 -27
  66. snowflake/cli/plugins/snowpark/manager.py +16 -5
  67. snowflake/cli/plugins/snowpark/models.py +6 -0
  68. snowflake/cli/plugins/sql/commands.py +3 -5
  69. snowflake/cli/plugins/sql/manager.py +1 -1
  70. snowflake/cli/plugins/stage/commands.py +2 -2
  71. snowflake/cli/plugins/stage/diff.py +4 -2
  72. snowflake/cli/plugins/stage/manager.py +290 -86
  73. snowflake/cli/plugins/streamlit/commands.py +20 -6
  74. snowflake/cli/plugins/streamlit/manager.py +29 -27
  75. snowflake/cli/plugins/workspace/__init__.py +13 -0
  76. snowflake/cli/plugins/workspace/commands.py +35 -0
  77. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  78. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
  79. snowflake/cli/templates/default_snowpark/app/common.py +0 -15
  80. snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
  81. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
  82. snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
  83. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
  84. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
  85. {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/METADATA +7 -6
  86. {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/RECORD +89 -69
  87. {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/WHEEL +0 -0
  88. {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/entry_points.txt +0 -0
  89. {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -24,12 +24,22 @@ from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
24
24
  ArtifactProcessor,
25
25
  UnsupportedArtifactProcessorError,
26
26
  )
27
+ from snowflake.cli.plugins.nativeapp.codegen.setup.native_app_setup_processor import (
28
+ NativeAppSetupProcessor,
29
+ )
27
30
  from snowflake.cli.plugins.nativeapp.codegen.snowpark.python_processor import (
28
31
  SnowparkAnnotationProcessor,
29
32
  )
33
+ from snowflake.cli.plugins.nativeapp.feature_flags import FeatureFlag
30
34
  from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
31
35
 
32
36
  SNOWPARK_PROCESSOR = "snowpark"
37
+ NA_SETUP_PROCESSOR = "native-app-setup"
38
+
39
+ _REGISTERED_PROCESSORS_BY_NAME = {
40
+ SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor,
41
+ NA_SETUP_PROCESSOR: NativeAppSetupProcessor,
42
+ }
33
43
 
34
44
 
35
45
  class NativeAppCompiler:
@@ -54,28 +64,31 @@ class NativeAppCompiler:
54
64
  Go through every artifact object in the project definition of a native app, and execute processors in order of specification for each of the artifact object.
55
65
  May have side-effects on the filesystem by either directly editing source files or the deploy root.
56
66
  """
57
- should_proceed = False
58
- for artifact in self._na_project.artifacts:
59
- if artifact.processors:
60
- should_proceed = True
61
- break
62
- if not should_proceed:
67
+
68
+ if not self._should_invoke_processors():
63
69
  return
64
70
 
65
71
  with cc.phase("Invoking artifact processors"):
72
+ if self._na_project.generated_root.exists():
73
+ raise ClickException(
74
+ f"Path {self._na_project.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
75
+ )
76
+
66
77
  for artifact in self._na_project.artifacts:
67
78
  for processor in artifact.processors:
68
- artifact_processor = self._try_create_processor(
69
- processor_mapping=processor,
70
- )
71
- if artifact_processor is None:
72
- raise UnsupportedArtifactProcessorError(
73
- processor_name=processor.name
74
- )
75
- else:
76
- artifact_processor.process(
77
- artifact_to_process=artifact, processor_mapping=processor
79
+ if self._is_enabled(processor):
80
+ artifact_processor = self._try_create_processor(
81
+ processor_mapping=processor,
78
82
  )
83
+ if artifact_processor is None:
84
+ raise UnsupportedArtifactProcessorError(
85
+ processor_name=processor.name
86
+ )
87
+ else:
88
+ artifact_processor.process(
89
+ artifact_to_process=artifact,
90
+ processor_mapping=processor,
91
+ )
79
92
 
80
93
  def _try_create_processor(
81
94
  self,
@@ -86,15 +99,32 @@ class NativeAppCompiler:
86
99
  Fetch processor object if one already exists in the cached_processors dictionary.
87
100
  Else, initialize a new object to return, and add it to the cached_processors dictionary.
88
101
  """
89
- if processor_mapping.name.lower() == SNOWPARK_PROCESSOR:
90
- curr_processor = self.cached_processors.get(SNOWPARK_PROCESSOR, None)
91
- if curr_processor is not None:
92
- return curr_processor
93
- else:
94
- curr_processor = SnowparkAnnotationProcessor(
95
- na_project=self._na_project,
96
- )
97
- self.cached_processors[SNOWPARK_PROCESSOR] = curr_processor
98
- return curr_processor
99
- else:
102
+ processor_name = processor_mapping.name.lower()
103
+ current_processor = self.cached_processors.get(processor_name)
104
+
105
+ if current_processor is not None:
106
+ return current_processor
107
+
108
+ processor_factory = _REGISTERED_PROCESSORS_BY_NAME.get(processor_name)
109
+ if processor_factory is None:
110
+ # No registered processor with the specified name
100
111
  return None
112
+
113
+ current_processor = processor_factory(
114
+ na_project=self._na_project,
115
+ )
116
+ self.cached_processors[processor_name] = current_processor
117
+
118
+ return current_processor
119
+
120
+ def _should_invoke_processors(self):
121
+ for artifact in self._na_project.artifacts:
122
+ for processor in artifact.processors:
123
+ if self._is_enabled(processor):
124
+ return True
125
+ return False
126
+
127
+ def _is_enabled(self, processor: ProcessorMapping) -> bool:
128
+ if processor.name.lower() == NA_SETUP_PROCESSOR:
129
+ return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled()
130
+ return True
@@ -20,10 +20,13 @@ import subprocess
20
20
  import sys
21
21
  from enum import Enum
22
22
  from pathlib import Path
23
- from typing import Optional, Sequence, Union
23
+ from typing import Any, Mapping, Optional, Sequence, Union
24
+ from venv import EnvBuilder
24
25
 
25
26
  from click.exceptions import ClickException
26
27
 
28
+ EnvVars = Mapping[str, str] # Only support str -> str for cross-platform compatibility
29
+
27
30
 
28
31
  class SandboxExecutionError(ClickException):
29
32
  """An error occurred while executing a python script."""
@@ -57,6 +60,7 @@ def _execute_python_interpreter(
57
60
  script_source: str,
58
61
  cwd: Optional[Union[str, Path]],
59
62
  timeout: Optional[int],
63
+ env_vars: Optional[EnvVars],
60
64
  ) -> subprocess.CompletedProcess:
61
65
  if not python_executable:
62
66
  raise SandboxExecutionError("No python executable found")
@@ -73,6 +77,7 @@ def _execute_python_interpreter(
73
77
  input=script_source,
74
78
  timeout=timeout,
75
79
  cwd=cwd,
80
+ env=env_vars,
76
81
  )
77
82
 
78
83
 
@@ -81,6 +86,7 @@ def _execute_in_venv(
81
86
  venv_path: Optional[Union[str, Path]] = None,
82
87
  cwd: Optional[Union[str, Path]] = None,
83
88
  timeout: Optional[int] = None,
89
+ env_vars: Optional[EnvVars] = None,
84
90
  ) -> subprocess.CompletedProcess:
85
91
  resolved_venv_path = None
86
92
  if venv_path is None:
@@ -114,7 +120,7 @@ def _execute_in_venv(
114
120
  )
115
121
 
116
122
  return _execute_python_interpreter(
117
- python_executable, script_source, timeout=timeout, cwd=cwd
123
+ python_executable, script_source, timeout=timeout, cwd=cwd, env_vars=env_vars
118
124
  )
119
125
 
120
126
 
@@ -123,6 +129,7 @@ def _execute_in_conda_env(
123
129
  env_name: Optional[str] = None,
124
130
  cwd: Optional[Union[str, Path]] = None,
125
131
  timeout: Optional[int] = None,
132
+ env_vars: Optional[EnvVars] = None,
126
133
  ) -> subprocess.CompletedProcess:
127
134
  conda_env = env_name
128
135
  if conda_env is None:
@@ -142,6 +149,7 @@ def _execute_in_conda_env(
142
149
  script_source,
143
150
  timeout=timeout,
144
151
  cwd=cwd,
152
+ env_vars=env_vars,
145
153
  )
146
154
 
147
155
 
@@ -149,13 +157,18 @@ def _execute_with_system_path_python(
149
157
  script_source: str,
150
158
  cwd: Optional[Union[str, Path]] = None,
151
159
  timeout: Optional[int] = None,
160
+ env_vars: Optional[EnvVars] = None,
152
161
  ) -> subprocess.CompletedProcess:
153
162
  python_executable = (
154
163
  shutil.which("python3") or shutil.which("python") or sys.executable
155
164
  )
156
165
 
157
166
  return _execute_python_interpreter(
158
- python_executable, script_source, timeout=timeout, cwd=cwd
167
+ python_executable,
168
+ script_source,
169
+ timeout=timeout,
170
+ cwd=cwd,
171
+ env_vars=env_vars,
159
172
  )
160
173
 
161
174
 
@@ -172,6 +185,7 @@ def execute_script_in_sandbox(
172
185
  env_type: ExecutionEnvironmentType = ExecutionEnvironmentType.AUTO_DETECT,
173
186
  cwd: Optional[Union[str, Path]] = None,
174
187
  timeout: Optional[int] = None,
188
+ env_vars: Optional[EnvVars] = None,
175
189
  **kwargs,
176
190
  ) -> subprocess.CompletedProcess:
177
191
  """
@@ -194,24 +208,99 @@ def execute_script_in_sandbox(
194
208
  """
195
209
  if env_type == ExecutionEnvironmentType.AUTO_DETECT:
196
210
  if _is_venv_active():
197
- return _execute_in_venv(script_source, cwd=cwd, timeout=timeout)
211
+ return _execute_in_venv(
212
+ script_source, cwd=cwd, timeout=timeout, env_vars=env_vars
213
+ )
198
214
  elif _is_conda_active():
199
- return _execute_in_conda_env(script_source, cwd=cwd, timeout=timeout)
215
+ return _execute_in_conda_env(
216
+ script_source, cwd=cwd, timeout=timeout, env_vars=env_vars
217
+ )
200
218
  else:
201
219
  return _execute_with_system_path_python(
202
- script_source, cwd=cwd, timeout=timeout
220
+ script_source, cwd=cwd, timeout=timeout, env_vars=env_vars
203
221
  )
204
222
  elif env_type == ExecutionEnvironmentType.VENV:
205
223
  return _execute_in_venv(
206
- script_source, kwargs.get("path"), cwd=cwd, timeout=timeout
224
+ script_source,
225
+ kwargs.get("path"),
226
+ cwd=cwd,
227
+ timeout=timeout,
228
+ env_vars=env_vars,
207
229
  )
208
230
  elif env_type == ExecutionEnvironmentType.CONDA:
209
231
  return _execute_in_conda_env(
210
- script_source, kwargs.get("name"), cwd=cwd, timeout=timeout
232
+ script_source,
233
+ kwargs.get("name"),
234
+ cwd=cwd,
235
+ timeout=timeout,
236
+ env_vars=env_vars,
211
237
  )
212
238
  elif env_type == ExecutionEnvironmentType.SYSTEM_PATH:
213
- return _execute_with_system_path_python(script_source, cwd=cwd, timeout=timeout)
239
+ return _execute_with_system_path_python(
240
+ script_source, cwd=cwd, timeout=timeout, env_vars=env_vars
241
+ )
214
242
  else: # ExecutionEnvironmentType.CURRENT
215
243
  return _execute_python_interpreter(
216
- sys.executable, script_source, cwd=cwd, timeout=timeout
244
+ sys.executable, script_source, cwd=cwd, timeout=timeout, env_vars=env_vars
217
245
  )
246
+
247
+
248
+ class SandboxEnvBuilder(EnvBuilder):
249
+ """
250
+ A virtual environment builder that can be used to build an environment suitable for
251
+ executing user-provided python scripts in an isolated sandbox.
252
+ """
253
+
254
+ def __init__(self, path: Path, **kwargs) -> None:
255
+ """
256
+ Creates a new builder with the specified destination path. The path need not
257
+ exist, it will be created when needed (recursively if necessary).
258
+
259
+ Parameters:
260
+ path (Path): The directory in which the sandbox environment will be created.
261
+ """
262
+ super().__init__(**kwargs)
263
+ self.path = path
264
+ self._context: Any = None # cached context
265
+
266
+ def post_setup(self, context) -> None:
267
+ self._context = context
268
+
269
+ def ensure_created(self) -> None:
270
+ """
271
+ Ensures that the sandbox environment has been created and correctly initialized.
272
+ """
273
+ if self.path.exists():
274
+ self._context = self.ensure_directories(self.path)
275
+ else:
276
+ self.path.mkdir(parents=True, exist_ok=True)
277
+ self.create(
278
+ self.path
279
+ ) # will set self._context through the post_setup callback
280
+
281
+ def run_python(self, *args) -> str:
282
+ """
283
+ Executes the python interpreter in the sandboxed environment with the provided arguments.
284
+ This raises a CalledProcessError if the python interpreter was not executed successfully.
285
+
286
+ Returns:
287
+ The output of running the command.
288
+ """
289
+ positional_args = [
290
+ self._context.env_exe,
291
+ "-E", # passing -E ignores all PYTHON* env vars
292
+ *args,
293
+ ]
294
+ kwargs = {
295
+ "cwd": self._context.env_dir,
296
+ "stderr": subprocess.STDOUT,
297
+ }
298
+ env = dict(os.environ)
299
+ env["VIRTUAL_ENV"] = self._context.env_dir
300
+ return subprocess.check_output(positional_args, **kwargs)
301
+
302
+ def pip_install(self, *args: Any) -> None:
303
+ """
304
+ Invokes pip install with the provided arguments.
305
+ """
306
+ self.run_python("-m", "pip", "install", *[str(arg) for arg in args])
@@ -0,0 +1,172 @@
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
+ import json
18
+ import os.path
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+ from click import ClickException
23
+ from snowflake.cli.api.console import cli_console as cc
24
+ from snowflake.cli.api.project.schemas.native_app.path_mapping import (
25
+ PathMapping,
26
+ ProcessorMapping,
27
+ )
28
+ from snowflake.cli.plugins.nativeapp.artifacts import BundleMap, find_setup_script_file
29
+ from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
30
+ ArtifactProcessor,
31
+ is_python_file_artifact,
32
+ )
33
+ from snowflake.cli.plugins.nativeapp.codegen.sandbox import (
34
+ ExecutionEnvironmentType,
35
+ SandboxEnvBuilder,
36
+ execute_script_in_sandbox,
37
+ )
38
+ from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
39
+ from snowflake.cli.plugins.stage.diff import to_stage_path
40
+
41
+ DEFAULT_TIMEOUT = 30
42
+ DRIVER_PATH = Path(__file__).parent / "setup_driver.py.source"
43
+
44
+
45
+ class NativeAppSetupProcessor(ArtifactProcessor):
46
+ def __init__(
47
+ self,
48
+ na_project: NativeAppProjectModel,
49
+ ):
50
+ super().__init__(na_project=na_project)
51
+
52
+ def process(
53
+ self,
54
+ artifact_to_process: PathMapping,
55
+ processor_mapping: Optional[ProcessorMapping],
56
+ **kwargs,
57
+ ) -> None:
58
+ """
59
+ Processes a Python setup script and generates the corresponding SQL commands.
60
+ """
61
+ bundle_map = BundleMap(
62
+ project_root=self._na_project.project_root,
63
+ deploy_root=self._na_project.deploy_root,
64
+ )
65
+ bundle_map.add(artifact_to_process)
66
+
67
+ self._create_or_update_sandbox()
68
+
69
+ cc.phase("Processing Python setup files")
70
+
71
+ files_to_process = []
72
+ for src_file, dest_file in bundle_map.all_mappings(
73
+ absolute=True, expand_directories=True, predicate=is_python_file_artifact
74
+ ):
75
+ cc.message(
76
+ f"Found Python setup file: {src_file.relative_to(self._na_project.project_root)}"
77
+ )
78
+ files_to_process.append(src_file)
79
+
80
+ sql_files_mapping = self._execute_in_sandbox(files_to_process)
81
+ self._generate_setup_sql(sql_files_mapping)
82
+
83
+ def _execute_in_sandbox(self, py_files: List[Path]) -> dict:
84
+ file_count = len(py_files)
85
+ cc.step(f"Processing {file_count} setup file{'s' if file_count > 1 else ''}")
86
+
87
+ env_vars = {
88
+ "_SNOWFLAKE_CLI_PROJECT_PATH": str(self._na_project.project_root),
89
+ "_SNOWFLAKE_CLI_SETUP_FILES": os.pathsep.join(map(str, py_files)),
90
+ "_SNOWFLAKE_CLI_APP_NAME": str(self._na_project.package_name),
91
+ "_SNOWFLAKE_CLI_SQL_DEST_DIR": str(self.generated_root),
92
+ }
93
+
94
+ try:
95
+ result = execute_script_in_sandbox(
96
+ script_source=DRIVER_PATH.read_text(),
97
+ env_type=ExecutionEnvironmentType.VENV,
98
+ cwd=self._na_project.bundle_root,
99
+ timeout=DEFAULT_TIMEOUT,
100
+ path=self.sandbox_root,
101
+ env_vars=env_vars,
102
+ )
103
+ except Exception as e:
104
+ raise ClickException(
105
+ f"Exception while executing python setup script logic: {e}"
106
+ )
107
+
108
+ if result.returncode == 0:
109
+ sql_file_mappings = json.loads(result.stdout)
110
+ return sql_file_mappings
111
+ else:
112
+ raise ClickException(
113
+ f"Failed to execute python setup script logic: {result.stderr}"
114
+ )
115
+
116
+ def _generate_setup_sql(self, sql_file_mappings: dict) -> None:
117
+ if not sql_file_mappings:
118
+ # Nothing to generate
119
+ return
120
+
121
+ generated_root = self.generated_root
122
+ generated_root.mkdir(exist_ok=True, parents=True)
123
+
124
+ cc.step("Patching setup script")
125
+ setup_file_path = find_setup_script_file(
126
+ deploy_root=self._na_project.deploy_root
127
+ )
128
+ with self.edit_file(setup_file_path) as f:
129
+ new_contents = [f.contents]
130
+
131
+ if sql_file_mappings["schemas"]:
132
+ schemas_file = generated_root / sql_file_mappings["schemas"]
133
+ new_contents.insert(
134
+ 0,
135
+ f"EXECUTE IMMEDIATE FROM '/{to_stage_path(schemas_file.relative_to(self._na_project.deploy_root))}';",
136
+ )
137
+
138
+ if sql_file_mappings["compute_pools"]:
139
+ compute_pools_file = generated_root / sql_file_mappings["compute_pools"]
140
+ new_contents.append(
141
+ f"EXECUTE IMMEDIATE FROM '/{to_stage_path(compute_pools_file.relative_to(self._na_project.deploy_root))}';"
142
+ )
143
+
144
+ if sql_file_mappings["services"]:
145
+ services_file = generated_root / sql_file_mappings["services"]
146
+ new_contents.append(
147
+ f"EXECUTE IMMEDIATE FROM '/{to_stage_path(services_file.relative_to(self._na_project.deploy_root))}';"
148
+ )
149
+
150
+ f.edited_contents = "\n".join(new_contents)
151
+
152
+ @property
153
+ def sandbox_root(self):
154
+ return self._na_project.bundle_root / "setup_py_venv"
155
+
156
+ @property
157
+ def generated_root(self):
158
+ return self._na_project.generated_root / "setup_py"
159
+
160
+ def _create_or_update_sandbox(self):
161
+ sandbox_root = self.sandbox_root
162
+ env_builder = SandboxEnvBuilder(sandbox_root, with_pip=True)
163
+ if sandbox_root.exists():
164
+ cc.step("Virtual environment found")
165
+ else:
166
+ cc.step(
167
+ f"Creating virtual environment in {sandbox_root.relative_to(self._na_project.project_root)}"
168
+ )
169
+ env_builder.ensure_created()
170
+
171
+ # Temporarily fetch the library from a location specified via env vars
172
+ env_builder.pip_install(os.environ["SNOWFLAKE_APP_PYTHON_LOC"])
@@ -0,0 +1,56 @@
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
+ import contextlib
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ import snowflake.app.context as ctx
21
+ from snowflake.app.sql import SQLGenerator
22
+
23
+ ctx._project_path = os.environ["_SNOWFLAKE_CLI_PROJECT_PATH"]
24
+ ctx._current_app_name = os.environ["_SNOWFLAKE_CLI_APP_NAME"]
25
+ __snowflake_internal_py_files = os.environ["_SNOWFLAKE_CLI_SETUP_FILES"].split(
26
+ os.pathsep
27
+ )
28
+ __snowflake_internal_sql_dest_dir = os.environ["_SNOWFLAKE_CLI_SQL_DEST_DIR"]
29
+
30
+ try:
31
+ import importlib
32
+
33
+ with contextlib.redirect_stdout(None):
34
+ with contextlib.redirect_stderr(None):
35
+ for __snowflake_internal_py_file in __snowflake_internal_py_files:
36
+ __snowflake_internal_spec = importlib.util.spec_from_file_location(
37
+ "<string>", __snowflake_internal_py_file
38
+ )
39
+ __snowflake_internal_module = importlib.util.module_from_spec(
40
+ __snowflake_internal_spec
41
+ )
42
+ __snowflake_internal_spec.loader.exec_module(
43
+ __snowflake_internal_module
44
+ )
45
+ except Exception as exc: # Catch any error
46
+ print("An exception occurred while executing file: ", exc, file=sys.stderr)
47
+ sys.exit(1)
48
+
49
+
50
+ import json
51
+
52
+ output_dir = Path(__snowflake_internal_sql_dest_dir)
53
+ output_dir.mkdir(exist_ok=True, parents=True)
54
+ path_mappings = SQLGenerator(dest_dir=output_dir).generate()
55
+
56
+ print(json.dumps(path_mappings, default=str))
@@ -20,19 +20,21 @@ from pathlib import Path
20
20
  from textwrap import dedent
21
21
  from typing import Any, Dict, List, Optional, Set
22
22
 
23
- from click import ClickException
24
23
  from pydantic import ValidationError
25
24
  from snowflake.cli.api.console import cli_console as cc
26
25
  from snowflake.cli.api.project.schemas.native_app.path_mapping import (
27
26
  PathMapping,
28
27
  ProcessorMapping,
29
28
  )
30
- from snowflake.cli.api.utils.rendering import jinja_render_from_file
29
+ from snowflake.cli.api.rendering.jinja import jinja_render_from_file
31
30
  from snowflake.cli.plugins.nativeapp.artifacts import (
32
31
  BundleMap,
33
32
  find_setup_script_file,
34
33
  )
35
- from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import ArtifactProcessor
34
+ from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
35
+ ArtifactProcessor,
36
+ is_python_file_artifact,
37
+ )
36
38
  from snowflake.cli.plugins.nativeapp.codegen.sandbox import (
37
39
  ExecutionEnvironmentType,
38
40
  SandboxExecutionError,
@@ -167,16 +169,6 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
167
169
  ):
168
170
  super().__init__(na_project=na_project)
169
171
 
170
- assert self._na_project.bundle_root.is_absolute()
171
- assert self._na_project.deploy_root.is_absolute()
172
- assert self._na_project.generated_root.is_absolute()
173
- assert self._na_project.project_root.is_absolute()
174
-
175
- if self._na_project.generated_root.exists():
176
- raise ClickException(
177
- f"Path {self._na_project.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
178
- )
179
-
180
172
  def process(
181
173
  self,
182
174
  artifact_to_process: PathMapping,
@@ -200,7 +192,9 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
200
192
 
201
193
  collected_output = []
202
194
  collected_sql_files: List[Path] = []
203
- for py_file, extension_fns in collected_extension_functions_by_path.items():
195
+ for py_file, extension_fns in sorted(
196
+ collected_extension_functions_by_path.items()
197
+ ):
204
198
  sql_file = self.generate_new_sql_file_name(
205
199
  py_file=py_file,
206
200
  )
@@ -236,9 +230,13 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
236
230
  edit_setup_script_with_exec_imm_sql(
237
231
  collected_sql_files=collected_sql_files,
238
232
  deploy_root=bundle_map.deploy_root(),
239
- generated_root=self._na_project.generated_root,
233
+ generated_root=self._generated_root,
240
234
  )
241
235
 
236
+ @property
237
+ def _generated_root(self):
238
+ return self._na_project.generated_root / "snowpark"
239
+
242
240
  def _normalize_imports(
243
241
  self,
244
242
  extension_fn: NativeAppExtensionFunction,
@@ -326,8 +324,12 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
326
324
  Path, List[NativeAppExtensionFunction]
327
325
  ] = {}
328
326
 
329
- for src_file, dest_file in bundle_map.all_mappings(
330
- absolute=True, expand_directories=True, predicate=_is_python_file_artifact
327
+ for src_file, dest_file in sorted(
328
+ bundle_map.all_mappings(
329
+ absolute=True,
330
+ expand_directories=True,
331
+ predicate=is_python_file_artifact,
332
+ )
331
333
  ):
332
334
  cc.step(
333
335
  "Processing Snowpark annotations from {}".format(
@@ -368,9 +370,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
368
370
  Generates a SQL filename for the generated root from the python file, and creates its parent directories.
369
371
  """
370
372
  relative_py_file = py_file.relative_to(self._na_project.deploy_root)
371
- sql_file = Path(
372
- self._na_project.generated_root, relative_py_file.with_suffix(".sql")
373
- )
373
+ sql_file = Path(self._generated_root, relative_py_file.with_suffix(".sql"))
374
374
  if sql_file.exists():
375
375
  cc.warning(
376
376
  f"""\
@@ -488,7 +488,7 @@ def edit_setup_script_with_exec_imm_sql(
488
488
  Adds an 'execute immediate' to setup script for every SQL file in the map
489
489
  """
490
490
  # Create a __generated.sql in the __generated folder
491
- generated_file_path = Path(generated_root, f"{generated_root.stem}.sql")
491
+ generated_file_path = Path(generated_root, f"__generated.sql")
492
492
  generated_file_path.parent.mkdir(exist_ok=True, parents=True)
493
493
 
494
494
  if generated_file_path.exists():