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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/api/cli_global_context.py +9 -0
- snowflake/cli/api/commands/decorators.py +9 -4
- snowflake/cli/api/commands/execution_metadata.py +40 -0
- snowflake/cli/api/commands/flags.py +45 -36
- snowflake/cli/api/commands/project_initialisation.py +4 -1
- snowflake/cli/api/commands/snow_typer.py +20 -9
- snowflake/cli/api/config.py +3 -0
- snowflake/cli/api/errno.py +27 -0
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/identifiers.py +20 -3
- snowflake/cli/api/output/types.py +9 -0
- snowflake/cli/api/project/definition_manager.py +2 -2
- snowflake/cli/api/project/project_verification.py +23 -0
- snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
- snowflake/cli/api/project/schemas/entities/common.py +85 -0
- snowflake/cli/api/project/schemas/entities/entities.py +30 -0
- snowflake/cli/api/project/schemas/project_definition.py +114 -22
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
- snowflake/cli/api/project/schemas/template.py +77 -0
- snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
- snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
- snowflake/cli/api/rendering/project_definition_templates.py +39 -0
- snowflake/cli/api/rendering/project_templates.py +97 -0
- snowflake/cli/api/rendering/sql_templates.py +56 -0
- snowflake/cli/api/rest_api.py +84 -25
- snowflake/cli/api/sql_execution.py +40 -1
- snowflake/cli/api/utils/definition_rendering.py +8 -5
- snowflake/cli/app/cli_app.py +0 -2
- snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
- snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
- snowflake/cli/app/loggers.py +10 -6
- snowflake/cli/app/printing.py +17 -7
- snowflake/cli/app/snow_connector.py +9 -1
- snowflake/cli/app/telemetry.py +41 -2
- snowflake/cli/plugins/connection/commands.py +4 -3
- snowflake/cli/plugins/connection/util.py +73 -18
- snowflake/cli/plugins/cortex/commands.py +2 -1
- snowflake/cli/plugins/git/commands.py +20 -4
- snowflake/cli/plugins/git/manager.py +44 -20
- snowflake/cli/plugins/init/__init__.py +13 -0
- snowflake/cli/plugins/init/commands.py +242 -0
- snowflake/cli/plugins/init/plugin_spec.py +30 -0
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
- snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
- snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
- snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
- snowflake/cli/plugins/nativeapp/commands.py +69 -6
- snowflake/cli/plugins/nativeapp/constants.py +0 -6
- snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
- snowflake/cli/plugins/nativeapp/init.py +1 -1
- snowflake/cli/plugins/nativeapp/manager.py +114 -39
- snowflake/cli/plugins/nativeapp/project_model.py +8 -4
- snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
- snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
- snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
- snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
- snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
- snowflake/cli/plugins/object/commands.py +1 -1
- snowflake/cli/plugins/object/manager.py +2 -15
- snowflake/cli/plugins/snowpark/commands.py +34 -26
- snowflake/cli/plugins/snowpark/common.py +88 -27
- snowflake/cli/plugins/snowpark/manager.py +16 -5
- snowflake/cli/plugins/snowpark/models.py +6 -0
- snowflake/cli/plugins/sql/commands.py +3 -5
- snowflake/cli/plugins/sql/manager.py +1 -1
- snowflake/cli/plugins/stage/commands.py +2 -2
- snowflake/cli/plugins/stage/diff.py +4 -2
- snowflake/cli/plugins/stage/manager.py +290 -86
- snowflake/cli/plugins/streamlit/commands.py +20 -6
- snowflake/cli/plugins/streamlit/manager.py +29 -27
- snowflake/cli/plugins/workspace/__init__.py +13 -0
- snowflake/cli/plugins/workspace/commands.py +35 -0
- snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
- snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
- snowflake/cli/templates/default_snowpark/app/common.py +0 -15
- snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
- snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
- snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
- snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
- snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
- {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/METADATA +7 -6
- {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/RECORD +89 -69
- {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-2.6.0rc0.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
330
|
-
|
|
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"
|
|
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():
|