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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/snow_connector.py +18 -11
- snowflake/cli/_plugins/connection/commands.py +3 -2
- snowflake/cli/_plugins/git/manager.py +14 -6
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
- snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -6
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
- snowflake/cli/_plugins/nativeapp/manager.py +29 -58
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
- snowflake/cli/_plugins/snowpark/commands.py +5 -65
- snowflake/cli/_plugins/snowpark/common.py +17 -1
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
- snowflake/cli/_plugins/sql/commands.py +1 -2
- snowflake/cli/_plugins/stage/commands.py +2 -2
- snowflake/cli/_plugins/stage/manager.py +46 -15
- snowflake/cli/_plugins/streamlit/commands.py +4 -63
- snowflake/cli/_plugins/streamlit/manager.py +4 -0
- snowflake/cli/_plugins/workspace/action_context.py +6 -0
- snowflake/cli/_plugins/workspace/commands.py +103 -22
- snowflake/cli/_plugins/workspace/manager.py +20 -4
- snowflake/cli/api/cli_global_context.py +6 -6
- snowflake/cli/api/commands/decorators.py +1 -1
- snowflake/cli/api/commands/flags.py +31 -12
- snowflake/cli/api/commands/snow_typer.py +9 -2
- snowflake/cli/api/config.py +17 -4
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/application_package_entity.py +296 -3
- snowflake/cli/api/entities/common.py +6 -2
- snowflake/cli/api/entities/utils.py +46 -10
- snowflake/cli/api/exceptions.py +12 -2
- snowflake/cli/api/feature_flags.py +0 -2
- snowflake/cli/api/project/definition.py +24 -1
- snowflake/cli/api/project/definition_conversion.py +194 -0
- snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
- snowflake/cli/api/project/schemas/project_definition.py +1 -4
- snowflake/cli/api/rendering/jinja.py +2 -16
- snowflake/cli/api/rendering/project_definition_templates.py +1 -1
- snowflake/cli/api/rendering/sql_templates.py +7 -4
- snowflake/cli/api/secure_path.py +13 -18
- snowflake/cli/api/secure_utils.py +90 -1
- snowflake/cli/api/sql_execution.py +13 -0
- snowflake/cli/api/utils/definition_rendering.py +4 -6
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -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 "
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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 / "
|
|
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.
|
|
24
|
-
ctx.
|
|
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.
|
|
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(
|
|
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
|
|
68
|
-
"""A referenced
|
|
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'
|
|
75
|
+
f'File "{relpath}{lineno_str}" does not contain a valid template: {err.message}'
|
|
76
76
|
)
|
|
77
77
|
self.err = err
|
|
78
78
|
|