snowflake-cli 3.2.2__py3-none-any.whl → 3.4.1__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/__main__.py +2 -2
- snowflake/cli/_app/cli_app.py +224 -192
- snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
- snowflake/cli/_app/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/cortex/commands.py +2 -4
- snowflake/cli/_plugins/git/manager.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
- snowflake/cli/_plugins/nativeapp/commands.py +113 -21
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
- snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
- snowflake/cli/_plugins/notebook/commands.py +55 -2
- snowflake/cli/_plugins/notebook/exceptions.py +1 -1
- snowflake/cli/_plugins/notebook/manager.py +7 -5
- snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
- snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
- snowflake/cli/_plugins/notebook/types.py +3 -0
- snowflake/cli/_plugins/snowpark/commands.py +48 -30
- snowflake/cli/_plugins/snowpark/common.py +47 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
- snowflake/cli/_plugins/snowpark/zipper.py +33 -1
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +131 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/commands.py +2 -1
- snowflake/cli/_plugins/stage/diff.py +60 -39
- snowflake/cli/_plugins/stage/manager.py +34 -13
- snowflake/cli/_plugins/stage/utils.py +1 -1
- snowflake/cli/_plugins/streamlit/commands.py +10 -1
- snowflake/cli/_plugins/streamlit/manager.py +70 -22
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
- snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
- snowflake/cli/_plugins/workspace/commands.py +6 -5
- snowflake/cli/_plugins/workspace/manager.py +9 -5
- snowflake/cli/api/artifacts/__init__.py +13 -0
- snowflake/cli/api/artifacts/bundle_map.py +500 -0
- snowflake/cli/api/artifacts/common.py +78 -0
- snowflake/cli/api/artifacts/utils.py +82 -0
- snowflake/cli/api/cli_global_context.py +36 -2
- snowflake/cli/api/commands/flags.py +10 -4
- snowflake/cli/api/commands/utils.py +28 -2
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +10 -1
- snowflake/cli/api/entities/common.py +81 -14
- snowflake/cli/api/entities/resolver.py +160 -0
- snowflake/cli/api/entities/utils.py +65 -23
- snowflake/cli/api/errno.py +63 -3
- snowflake/cli/api/feature_flags.py +19 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +4 -4
- snowflake/cli/api/project/project_paths.py +28 -0
- snowflake/cli/api/project/schemas/entities/common.py +130 -1
- snowflake/cli/api/project/schemas/entities/entities.py +4 -0
- snowflake/cli/api/project/schemas/project_definition.py +54 -6
- snowflake/cli/api/project/schemas/updatable_model.py +2 -2
- snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- snowflake/cli/api/secure_path.py +6 -0
- snowflake/cli/api/sql_execution.py +5 -1
- snowflake/cli/api/stage_path.py +7 -2
- snowflake/cli/api/utils/graph.py +3 -0
- snowflake/cli/api/utils/path_utils.py +24 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
- snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,241 @@
|
|
|
1
|
-
from
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Generic, List, Optional, TypeVar
|
|
2
4
|
|
|
5
|
+
from click import ClickException
|
|
6
|
+
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
|
|
7
|
+
from snowflake.cli._plugins.snowpark import package_utils
|
|
8
|
+
from snowflake.cli._plugins.snowpark.common import (
|
|
9
|
+
DEFAULT_RUNTIME,
|
|
10
|
+
map_path_mapping_to_artifact,
|
|
11
|
+
zip_and_copy_artifacts_to_deploy,
|
|
12
|
+
)
|
|
13
|
+
from snowflake.cli._plugins.snowpark.package.anaconda_packages import (
|
|
14
|
+
AnacondaPackages,
|
|
15
|
+
AnacondaPackagesManager,
|
|
16
|
+
)
|
|
17
|
+
from snowflake.cli._plugins.snowpark.package_utils import (
|
|
18
|
+
DownloadUnavailablePackagesResult,
|
|
19
|
+
)
|
|
3
20
|
from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
|
|
4
21
|
FunctionEntityModel,
|
|
5
22
|
ProcedureEntityModel,
|
|
6
23
|
)
|
|
24
|
+
from snowflake.cli._plugins.snowpark.snowpark_project_paths import SnowparkProjectPaths
|
|
25
|
+
from snowflake.cli._plugins.snowpark.zipper import zip_dir
|
|
26
|
+
from snowflake.cli._plugins.workspace.context import ActionContext
|
|
7
27
|
from snowflake.cli.api.entities.common import EntityBase
|
|
28
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
29
|
+
from snowflake.connector import ProgrammingError
|
|
8
30
|
|
|
9
31
|
T = TypeVar("T")
|
|
10
32
|
|
|
11
33
|
|
|
34
|
+
class CreateMode(
|
|
35
|
+
str, Enum
|
|
36
|
+
): # This should probably be moved to some common place, think where
|
|
37
|
+
create = "CREATE"
|
|
38
|
+
create_or_replace = "CREATE OR REPLACE"
|
|
39
|
+
create_if_not_exists = "CREATE IF NOT EXISTS"
|
|
40
|
+
|
|
41
|
+
|
|
12
42
|
class SnowparkEntity(EntityBase[Generic[T]]):
|
|
13
|
-
|
|
43
|
+
def __init__(self, *args, **kwargs):
|
|
44
|
+
|
|
45
|
+
if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
|
|
46
|
+
raise NotImplementedError("Snowpark entity is not implemented yet")
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def action_bundle(
|
|
50
|
+
self,
|
|
51
|
+
action_ctx: ActionContext,
|
|
52
|
+
ignore_anaconda: bool,
|
|
53
|
+
skip_version_check: bool,
|
|
54
|
+
output_dir: Path | None = None,
|
|
55
|
+
index_url: str | None = None,
|
|
56
|
+
allow_shared_libraries: bool = False,
|
|
57
|
+
*args,
|
|
58
|
+
**kwargs,
|
|
59
|
+
) -> List[Path]:
|
|
60
|
+
return self.bundle(
|
|
61
|
+
ignore_anaconda,
|
|
62
|
+
skip_version_check,
|
|
63
|
+
output_dir,
|
|
64
|
+
index_url,
|
|
65
|
+
allow_shared_libraries,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def action_deploy(
|
|
69
|
+
self,
|
|
70
|
+
action_ctx: ActionContext,
|
|
71
|
+
mode: CreateMode = CreateMode.create,
|
|
72
|
+
*args,
|
|
73
|
+
**kwargs,
|
|
74
|
+
):
|
|
75
|
+
# TODO: After introducing bundle map, we should introduce file copying part here
|
|
76
|
+
return self.deploy(mode, *args, **kwargs)
|
|
77
|
+
|
|
78
|
+
def action_drop(self, action_ctx: ActionContext, *args, **kwargs):
|
|
79
|
+
return self._execute_query(self.get_drop_sql())
|
|
80
|
+
|
|
81
|
+
def action_describe(self, action_ctx: ActionContext, *args, **kwargs):
|
|
82
|
+
return self._execute_query(self.get_describe_sql())
|
|
83
|
+
|
|
84
|
+
def action_execute(
|
|
85
|
+
self,
|
|
86
|
+
action_ctx: ActionContext,
|
|
87
|
+
execution_arguments: List[str] | None = None,
|
|
88
|
+
*args,
|
|
89
|
+
**kwargs,
|
|
90
|
+
):
|
|
91
|
+
return self._execute_query(self.get_execute_sql(execution_arguments))
|
|
92
|
+
|
|
93
|
+
def bundle(
|
|
94
|
+
self,
|
|
95
|
+
ignore_anaconda: bool,
|
|
96
|
+
skip_version_check: bool,
|
|
97
|
+
output_dir: Path | None = None,
|
|
98
|
+
index_url: str | None = None,
|
|
99
|
+
allow_shared_libraries: bool = False,
|
|
100
|
+
) -> List[Path]:
|
|
101
|
+
"""
|
|
102
|
+
Bundles the entity artifacts and dependencies into a directory.
|
|
103
|
+
Parameters:
|
|
104
|
+
output_dir: The directory to output the bundled artifacts to. Defaults to output dir in project root
|
|
105
|
+
ignore_anaconda: If True, ignores anaconda check and tries to download all packages using pip
|
|
106
|
+
skip_version_check: If True, skips version check when downloading packages
|
|
107
|
+
index_url: The index URL to use when downloading packages, if none set - default pip index is used (in most cases- Pypi)
|
|
108
|
+
allow_shared_libraries: If not set to True, using dependency with .so/.dll files will raise an exception
|
|
109
|
+
Returns:
|
|
110
|
+
"""
|
|
111
|
+
# 0 Create a directory for the entity
|
|
112
|
+
project_paths = SnowparkProjectPaths(
|
|
113
|
+
project_root=self.root.absolute(),
|
|
114
|
+
)
|
|
115
|
+
if not output_dir:
|
|
116
|
+
output_dir = project_paths.bundle_root
|
|
117
|
+
if not output_dir.exists(): # type: ignore[union-attr]
|
|
118
|
+
SecurePath(output_dir).mkdir(parents=True)
|
|
119
|
+
|
|
120
|
+
# 1 Check if requirements exits
|
|
121
|
+
if (self.root / "requirements.txt").exists():
|
|
122
|
+
download_results = self._process_requirements(
|
|
123
|
+
bundle_dir=output_dir, # type: ignore
|
|
124
|
+
archive_name="dependencies.zip",
|
|
125
|
+
requirements_file=SecurePath(self.root / "requirements.txt"),
|
|
126
|
+
ignore_anaconda=ignore_anaconda,
|
|
127
|
+
skip_version_check=skip_version_check,
|
|
128
|
+
index_url=index_url,
|
|
129
|
+
allow_shared_libraries=allow_shared_libraries,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 2 get the artifacts list
|
|
133
|
+
artifacts = map_path_mapping_to_artifact(project_paths, self.model.artifacts)
|
|
134
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
135
|
+
|
|
136
|
+
if FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_enabled():
|
|
137
|
+
return zip_and_copy_artifacts_to_deploy(
|
|
138
|
+
artifacts, project_paths.bundle_root
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
copied_files = []
|
|
142
|
+
for artifact in artifacts:
|
|
143
|
+
artifact.build()
|
|
144
|
+
copied_files.append(artifact.post_build_path)
|
|
145
|
+
return copied_files
|
|
146
|
+
|
|
147
|
+
def check_if_exists(
|
|
148
|
+
self, action_ctx: ActionContext
|
|
149
|
+
) -> bool: # TODO it should return current state, so we know if update is necessary
|
|
150
|
+
try:
|
|
151
|
+
current_state = self.action_describe(action_ctx)
|
|
152
|
+
return True
|
|
153
|
+
except ProgrammingError:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def deploy(self, mode: CreateMode = CreateMode.create, *args, **kwargs):
|
|
157
|
+
return self._execute_query(self.get_deploy_sql(mode))
|
|
158
|
+
|
|
159
|
+
def get_deploy_sql(self, mode: CreateMode):
|
|
160
|
+
query = [
|
|
161
|
+
f"{mode.value} {self.model.type.upper()} {self.identifier}",
|
|
162
|
+
"COPY GRANTS",
|
|
163
|
+
f"RETURNS {self.model.returns}",
|
|
164
|
+
f"LANGUAGE PYTHON",
|
|
165
|
+
f"RUNTIME_VERSION '{self.model.runtime or DEFAULT_RUNTIME}'",
|
|
166
|
+
f"IMPORTS={','.join(self.model.imports)}", # TODO: Add source files here after introducing bundlemap
|
|
167
|
+
f"HANDLER='{self.model.handler}'",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
if self.model.external_access_integrations:
|
|
171
|
+
query.append(self.model.get_external_access_integrations_sql())
|
|
172
|
+
|
|
173
|
+
if self.model.secrets:
|
|
174
|
+
query.append(self.model.get_secrets_sql())
|
|
175
|
+
|
|
176
|
+
if self.model.type == "procedure" and self.model.execute_as_caller:
|
|
177
|
+
query.append("EXECUTE AS CALLER")
|
|
178
|
+
|
|
179
|
+
return "\n".join(query)
|
|
180
|
+
|
|
181
|
+
def get_execute_sql(self, execution_arguments: List[str] | None = None):
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
def _process_requirements( # TODO: maybe leave all the logic with requirements here - so download, write requirements file etc.
|
|
185
|
+
self,
|
|
186
|
+
bundle_dir: Path,
|
|
187
|
+
archive_name: str, # TODO: not the best name, think of something else
|
|
188
|
+
requirements_file: Optional[SecurePath],
|
|
189
|
+
ignore_anaconda: bool,
|
|
190
|
+
skip_version_check: bool = False,
|
|
191
|
+
index_url: Optional[str] = None,
|
|
192
|
+
allow_shared_libraries: bool = False,
|
|
193
|
+
) -> DownloadUnavailablePackagesResult:
|
|
194
|
+
"""
|
|
195
|
+
Processes the requirements file and downloads the dependencies
|
|
196
|
+
Parameters:
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
anaconda_packages_manager = AnacondaPackagesManager()
|
|
200
|
+
with SecurePath.temporary_directory() as tmp_dir:
|
|
201
|
+
requirements = package_utils.parse_requirements(requirements_file)
|
|
202
|
+
anaconda_packages = (
|
|
203
|
+
AnacondaPackages.empty()
|
|
204
|
+
if ignore_anaconda
|
|
205
|
+
else anaconda_packages_manager.find_packages_available_in_snowflake_anaconda()
|
|
206
|
+
)
|
|
207
|
+
download_result = package_utils.download_unavailable_packages(
|
|
208
|
+
requirements=requirements,
|
|
209
|
+
target_dir=tmp_dir,
|
|
210
|
+
anaconda_packages=anaconda_packages,
|
|
211
|
+
skip_version_check=skip_version_check,
|
|
212
|
+
pip_index_url=index_url,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if download_result.anaconda_packages:
|
|
216
|
+
anaconda_packages.write_requirements_file_in_snowflake_format(
|
|
217
|
+
file_path=SecurePath(bundle_dir / "requirements.txt"),
|
|
218
|
+
requirements=download_result.anaconda_packages,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if download_result.downloaded_packages_details:
|
|
222
|
+
if (
|
|
223
|
+
package_utils.detect_and_log_shared_libraries(
|
|
224
|
+
download_result.downloaded_packages_details
|
|
225
|
+
)
|
|
226
|
+
and not allow_shared_libraries
|
|
227
|
+
):
|
|
228
|
+
raise ClickException(
|
|
229
|
+
"Some packages contain shared (.so/.dll) libraries. "
|
|
230
|
+
"Try again with allow_shared_libraries_flag."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
zip_dir(
|
|
234
|
+
source=tmp_dir,
|
|
235
|
+
dest_zip=bundle_dir / archive_name,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return download_result
|
|
14
239
|
|
|
15
240
|
|
|
16
241
|
class FunctionEntity(SnowparkEntity[FunctionEntityModel]):
|
|
@@ -18,7 +243,17 @@ class FunctionEntity(SnowparkEntity[FunctionEntityModel]):
|
|
|
18
243
|
A single UDF
|
|
19
244
|
"""
|
|
20
245
|
|
|
21
|
-
|
|
246
|
+
# TO THINK OF
|
|
247
|
+
# Where will we get imports? Should we rely on bundle map? Or should it be self-sufficient in this matter?
|
|
248
|
+
|
|
249
|
+
def get_execute_sql(
|
|
250
|
+
self, execution_arguments: List[str] | None = None, *args, **kwargs
|
|
251
|
+
):
|
|
252
|
+
if not execution_arguments:
|
|
253
|
+
execution_arguments = []
|
|
254
|
+
return (
|
|
255
|
+
f"SELECT {self.fqn}({', '.join([str(arg) for arg in execution_arguments])})"
|
|
256
|
+
)
|
|
22
257
|
|
|
23
258
|
|
|
24
259
|
class ProcedureEntity(SnowparkEntity[ProcedureEntityModel]):
|
|
@@ -26,4 +261,12 @@ class ProcedureEntity(SnowparkEntity[ProcedureEntityModel]):
|
|
|
26
261
|
A stored procedure
|
|
27
262
|
"""
|
|
28
263
|
|
|
29
|
-
|
|
264
|
+
def get_execute_sql(
|
|
265
|
+
self,
|
|
266
|
+
execution_arguments: List[str] | None = None,
|
|
267
|
+
):
|
|
268
|
+
if not execution_arguments:
|
|
269
|
+
execution_arguments = []
|
|
270
|
+
return (
|
|
271
|
+
f"CALL {self.fqn}({', '.join([str(arg) for arg in execution_arguments])})"
|
|
272
|
+
)
|
|
@@ -14,37 +14,27 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import glob
|
|
18
18
|
from typing import List, Literal, Optional, Union
|
|
19
19
|
|
|
20
20
|
from pydantic import Field, field_validator
|
|
21
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
21
22
|
from snowflake.cli.api.identifiers import FQN
|
|
22
23
|
from snowflake.cli.api.project.schemas.entities.common import (
|
|
23
|
-
|
|
24
|
+
EntityModelBaseWithArtifacts,
|
|
24
25
|
ExternalAccessBaseModel,
|
|
25
26
|
ImportsBaseModel,
|
|
27
|
+
PathMapping,
|
|
26
28
|
)
|
|
27
29
|
from snowflake.cli.api.project.schemas.updatable_model import (
|
|
28
30
|
DiscriminatorField,
|
|
29
|
-
UpdatableModel,
|
|
30
31
|
)
|
|
31
32
|
from snowflake.cli.api.project.schemas.v1.snowpark.argument import Argument
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
src: Path = Field(title="Source path (relative to project root)", default=None)
|
|
39
|
-
|
|
40
|
-
dest: Optional[str] = Field(
|
|
41
|
-
title="Destination path on stage",
|
|
42
|
-
description="Paths are relative to stage root; paths ending with a slash indicate that the destination is a directory which source files should be copied into.",
|
|
43
|
-
default=None,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class SnowparkEntityModel(EntityModelBase, ExternalAccessBaseModel, ImportsBaseModel):
|
|
35
|
+
class SnowparkEntityModel(
|
|
36
|
+
EntityModelBaseWithArtifacts, ExternalAccessBaseModel, ImportsBaseModel
|
|
37
|
+
):
|
|
48
38
|
handler: str = Field(
|
|
49
39
|
title="Function’s or procedure’s implementation of the object inside source module",
|
|
50
40
|
examples=["functions.hello_function"],
|
|
@@ -59,17 +49,23 @@ class SnowparkEntityModel(EntityModelBase, ExternalAccessBaseModel, ImportsBaseM
|
|
|
59
49
|
title="Python version to use when executing ", default=None
|
|
60
50
|
)
|
|
61
51
|
stage: str = Field(title="Stage in which artifacts will be stored")
|
|
62
|
-
artifacts: List[Union[PathMapping, str]] = Field(title="List of required sources")
|
|
63
52
|
|
|
64
53
|
@field_validator("artifacts")
|
|
65
54
|
@classmethod
|
|
66
55
|
def _convert_artifacts(cls, artifacts: Union[dict, str]):
|
|
67
56
|
_artifacts = []
|
|
68
|
-
for
|
|
69
|
-
if
|
|
70
|
-
|
|
57
|
+
for artifact in artifacts:
|
|
58
|
+
if (
|
|
59
|
+
(isinstance(artifact, str) and glob.has_magic(artifact))
|
|
60
|
+
or (isinstance(artifact, PathMapping) and glob.has_magic(artifact.src))
|
|
61
|
+
) and FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_disabled():
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"If you want to use glob patterns in artifacts, you need to enable the Snowpark new build feature flag (enable_snowpark_glob_support=true)"
|
|
64
|
+
)
|
|
65
|
+
if isinstance(artifact, PathMapping):
|
|
66
|
+
_artifacts.append(artifact)
|
|
71
67
|
else:
|
|
72
|
-
_artifacts.append(PathMapping(src=
|
|
68
|
+
_artifacts.append(PathMapping(src=artifact))
|
|
73
69
|
return _artifacts
|
|
74
70
|
|
|
75
71
|
@field_validator("runtime")
|
|
@@ -79,14 +75,6 @@ class SnowparkEntityModel(EntityModelBase, ExternalAccessBaseModel, ImportsBaseM
|
|
|
79
75
|
return str(runtime_input)
|
|
80
76
|
return runtime_input
|
|
81
77
|
|
|
82
|
-
@field_validator("artifacts")
|
|
83
|
-
@classmethod
|
|
84
|
-
def validate_artifacts(cls, artifacts: List[Path]) -> List[Path]:
|
|
85
|
-
for artefact in artifacts:
|
|
86
|
-
if "*" in str(artefact):
|
|
87
|
-
raise ValueError("Glob patterns not supported for Snowpark artifacts.")
|
|
88
|
-
return artifacts
|
|
89
|
-
|
|
90
78
|
@property
|
|
91
79
|
def udf_sproc_identifier(self) -> UdfSprocIdentifier:
|
|
92
80
|
return UdfSprocIdentifier.from_definition(self)
|
|
@@ -13,38 +13,60 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import glob
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
16
19
|
from dataclasses import dataclass
|
|
17
20
|
from pathlib import Path, PurePosixPath
|
|
21
|
+
from typing import Optional
|
|
18
22
|
|
|
19
|
-
from snowflake.cli._plugins.snowpark.snowpark_entity_model import PathMapping
|
|
20
23
|
from snowflake.cli._plugins.snowpark.zipper import zip_dir
|
|
21
24
|
from snowflake.cli.api.console import cli_console
|
|
22
25
|
from snowflake.cli.api.constants import DEPLOYMENT_STAGE
|
|
26
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
23
27
|
from snowflake.cli.api.identifiers import FQN
|
|
28
|
+
from snowflake.cli.api.project.project_paths import ProjectPaths, bundle_root
|
|
29
|
+
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
24
30
|
from snowflake.cli.api.secure_path import SecurePath
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
@dataclass
|
|
28
|
-
class SnowparkProjectPaths:
|
|
34
|
+
class SnowparkProjectPaths(ProjectPaths):
|
|
29
35
|
"""
|
|
30
|
-
This class
|
|
36
|
+
This class allows you to manage files paths related to given project.
|
|
31
37
|
"""
|
|
32
38
|
|
|
33
|
-
project_root: Path
|
|
34
|
-
|
|
35
39
|
def path_relative_to_root(self, artifact_path: Path) -> Path:
|
|
36
40
|
if artifact_path.is_absolute():
|
|
37
41
|
return artifact_path
|
|
38
42
|
return (self.project_root / artifact_path).resolve()
|
|
39
43
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
def get_artifact_dto(self, artifact_path: PathMapping) -> Artifact:
|
|
45
|
+
if FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_enabled():
|
|
46
|
+
return Artifact(
|
|
47
|
+
project_root=self.project_root,
|
|
48
|
+
bundle_root=self.bundle_root,
|
|
49
|
+
dest=artifact_path.dest,
|
|
50
|
+
path=Path(artifact_path.src),
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
return ArtifactOldBuild(
|
|
54
|
+
dest=artifact_path.dest,
|
|
55
|
+
path=self.path_relative_to_root(Path(artifact_path.src)),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get_dependencies_artifact(self) -> Artifact:
|
|
59
|
+
if FeatureFlag.ENABLE_SNOWPARK_GLOB_SUPPORT.is_enabled():
|
|
60
|
+
return Artifact(
|
|
61
|
+
project_root=self.project_root,
|
|
62
|
+
bundle_root=self.bundle_root,
|
|
63
|
+
dest=None,
|
|
64
|
+
path=Path("dependencies.zip"),
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
return ArtifactOldBuild(
|
|
68
|
+
dest=None, path=self.path_relative_to_root(Path("dependencies.zip"))
|
|
69
|
+
)
|
|
48
70
|
|
|
49
71
|
@property
|
|
50
72
|
def snowflake_requirements(self) -> SecurePath:
|
|
@@ -57,19 +79,130 @@ class SnowparkProjectPaths:
|
|
|
57
79
|
return SecurePath(self.path_relative_to_root(Path("requirements.txt")))
|
|
58
80
|
|
|
59
81
|
@property
|
|
60
|
-
def
|
|
61
|
-
return self.
|
|
82
|
+
def bundle_root(self) -> Path:
|
|
83
|
+
return bundle_root(self.project_root, "snowpark")
|
|
62
84
|
|
|
63
85
|
|
|
64
86
|
@dataclass(unsafe_hash=True)
|
|
65
|
-
class
|
|
66
|
-
"""Helper for getting paths related to given
|
|
87
|
+
class Artifact:
|
|
88
|
+
"""Helper for getting paths related to given artifact."""
|
|
67
89
|
|
|
90
|
+
project_root: Path
|
|
91
|
+
bundle_root: Path
|
|
68
92
|
path: Path
|
|
69
93
|
dest: str | None = None
|
|
70
94
|
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
project_root: Path,
|
|
98
|
+
bundle_root: Path,
|
|
99
|
+
path: Path,
|
|
100
|
+
dest: Optional[str] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self.project_root = project_root
|
|
103
|
+
self.bundle_root = bundle_root
|
|
104
|
+
self.path = path
|
|
105
|
+
self.dest = dest
|
|
106
|
+
if self.dest and not self._is_dest_a_file() and not self.dest.endswith("/"):
|
|
107
|
+
self.dest = self.dest + "/"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def _artifact_name(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Returns artifact name. Directories are mapped to corresponding .zip files.
|
|
113
|
+
For paths with glob patterns, the last part of the path is used.
|
|
114
|
+
For files, the file name is used.
|
|
115
|
+
"""
|
|
116
|
+
if glob.has_magic(str(self.path)):
|
|
117
|
+
last_part = None
|
|
118
|
+
for part in self.path.parts:
|
|
119
|
+
if glob.has_magic(part):
|
|
120
|
+
break
|
|
121
|
+
else:
|
|
122
|
+
last_part = part
|
|
123
|
+
if not last_part:
|
|
124
|
+
last_part = os.path.commonpath(
|
|
125
|
+
[str(self.path), str(self.path.absolute())]
|
|
126
|
+
)
|
|
127
|
+
return last_part + ".zip"
|
|
128
|
+
if (self.project_root / self.path).is_dir():
|
|
129
|
+
return self.path.stem + ".zip"
|
|
130
|
+
if (self.project_root / self.path).is_file() and self._is_dest_a_file():
|
|
131
|
+
return Path(self.dest).name # type: ignore
|
|
132
|
+
return self.path.name
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def post_build_path(self) -> Path:
|
|
136
|
+
"""
|
|
137
|
+
Returns post-build artifact path. Directories are mapped to corresponding .zip files.
|
|
138
|
+
"""
|
|
139
|
+
bundle_root = self.bundle_root
|
|
140
|
+
path = (
|
|
141
|
+
self._path_until_asterisk()
|
|
142
|
+
if glob.has_magic(str(self.path))
|
|
143
|
+
else self.path.parent
|
|
144
|
+
)
|
|
145
|
+
if self._is_dest_a_file():
|
|
146
|
+
return bundle_root / self.dest # type: ignore
|
|
147
|
+
return bundle_root / (self.dest or path) / self._artifact_name
|
|
148
|
+
|
|
149
|
+
def upload_path(self, stage: FQN | str | None) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Path on stage to which the artifact should be uploaded.
|
|
152
|
+
"""
|
|
153
|
+
stage = stage or DEPLOYMENT_STAGE
|
|
154
|
+
if isinstance(stage, str):
|
|
155
|
+
stage = FQN.from_stage(stage).using_context()
|
|
156
|
+
|
|
157
|
+
stage_path = PurePosixPath(f"@{stage}")
|
|
158
|
+
if self.dest:
|
|
159
|
+
stage_path /= (
|
|
160
|
+
PurePosixPath(self.dest).parent if self._is_dest_a_file() else self.dest
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
stage_path /= (
|
|
164
|
+
self._path_until_asterisk()
|
|
165
|
+
if glob.has_magic(str(self.path))
|
|
166
|
+
else PurePosixPath(self.path).parent
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return str(stage_path) + "/"
|
|
170
|
+
|
|
171
|
+
def import_path(self, stage: FQN | str | None) -> str:
|
|
172
|
+
"""Path for UDF/sproc imports clause."""
|
|
173
|
+
return self.upload_path(stage) + self._artifact_name
|
|
174
|
+
|
|
175
|
+
def _is_dest_a_file(self) -> bool:
|
|
176
|
+
if not self.dest:
|
|
177
|
+
return False
|
|
178
|
+
return re.search(r"\.[a-zA-Z0-9]{2,4}$", self.dest) is not None
|
|
179
|
+
|
|
180
|
+
def _path_until_asterisk(self) -> Path:
|
|
181
|
+
path = []
|
|
182
|
+
for part in self.path.parts:
|
|
183
|
+
if glob.has_magic(part):
|
|
184
|
+
break
|
|
185
|
+
else:
|
|
186
|
+
path.append(part)
|
|
187
|
+
return Path(*path[:-1])
|
|
188
|
+
|
|
189
|
+
# Can be removed after removing ENABLE_SNOWPARK_GLOB_SUPPORT feature flag.
|
|
190
|
+
def build(self) -> None:
|
|
191
|
+
raise NotImplementedError("Not implemented in Artifact class.")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(unsafe_hash=True)
|
|
195
|
+
class ArtifactOldBuild(Artifact):
|
|
196
|
+
"""Helper for getting paths related to given artifact."""
|
|
197
|
+
|
|
198
|
+
path: Path
|
|
199
|
+
dest: str | None = None
|
|
200
|
+
|
|
201
|
+
def __init__(self, path: Path, dest: Optional[str] = None) -> None:
|
|
202
|
+
super().__init__(project_root=Path(), bundle_root=Path(), path=path, dest=dest)
|
|
203
|
+
|
|
71
204
|
@property
|
|
72
|
-
def
|
|
205
|
+
def _artifact_name(self) -> str:
|
|
73
206
|
if self.path.is_dir():
|
|
74
207
|
return self.path.stem + ".zip"
|
|
75
208
|
return self.path.name
|
|
@@ -77,13 +210,13 @@ class Artefact:
|
|
|
77
210
|
@property
|
|
78
211
|
def post_build_path(self) -> Path:
|
|
79
212
|
"""
|
|
80
|
-
Returns post-build
|
|
213
|
+
Returns post-build artifact path. Directories are mapped to corresponding .zip files.
|
|
81
214
|
"""
|
|
82
|
-
return self.path.parent / self.
|
|
215
|
+
return self.path.parent / self._artifact_name
|
|
83
216
|
|
|
84
217
|
def upload_path(self, stage: FQN | str | None) -> str:
|
|
85
218
|
"""
|
|
86
|
-
Path on stage to which the
|
|
219
|
+
Path on stage to which the artifact should be uploaded.
|
|
87
220
|
"""
|
|
88
221
|
stage = stage or DEPLOYMENT_STAGE
|
|
89
222
|
if isinstance(stage, str):
|
|
@@ -96,10 +229,10 @@ class Artefact:
|
|
|
96
229
|
|
|
97
230
|
def import_path(self, stage: FQN | str | None) -> str:
|
|
98
231
|
"""Path for UDF/sproc imports clause."""
|
|
99
|
-
return self.upload_path(stage) + self.
|
|
232
|
+
return self.upload_path(stage) + self._artifact_name
|
|
100
233
|
|
|
101
234
|
def build(self) -> None:
|
|
102
|
-
"""Build the
|
|
235
|
+
"""Build the artifact. Applies only to directories. Files are untouched."""
|
|
103
236
|
if not self.path.is_dir():
|
|
104
237
|
return
|
|
105
238
|
cli_console.step(f"Creating: {self.post_build_path.name}")
|
|
@@ -20,6 +20,10 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Dict, List, Literal
|
|
21
21
|
from zipfile import ZIP_DEFLATED, ZipFile
|
|
22
22
|
|
|
23
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
24
|
+
from snowflake.cli.api.console import cli_console
|
|
25
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
26
|
+
|
|
23
27
|
log = logging.getLogger(__name__)
|
|
24
28
|
|
|
25
29
|
IGNORED_FILES = [
|
|
@@ -64,7 +68,10 @@ def zip_dir(
|
|
|
64
68
|
mode: Literal["r", "w", "x", "a"] = "w",
|
|
65
69
|
) -> None:
|
|
66
70
|
|
|
67
|
-
if
|
|
71
|
+
if not dest_zip.parent.exists():
|
|
72
|
+
SecurePath(dest_zip).parent.mkdir(parents=True)
|
|
73
|
+
|
|
74
|
+
if isinstance(source, Path) or isinstance(source, SecurePath):
|
|
68
75
|
source = [source]
|
|
69
76
|
|
|
70
77
|
files_to_pack: Dict[Path, List[Path]] = {
|
|
@@ -74,11 +81,36 @@ def zip_dir(
|
|
|
74
81
|
|
|
75
82
|
with ZipFile(dest_zip, mode, ZIP_DEFLATED, allowZip64=True) as package_zip:
|
|
76
83
|
for src, files in files_to_pack.items():
|
|
84
|
+
if isinstance(src, SecurePath):
|
|
85
|
+
src = src.path
|
|
77
86
|
for file in files:
|
|
78
87
|
log.debug("Adding %s to %s", file, dest_zip)
|
|
79
88
|
package_zip.write(file, arcname=file.relative_to(src))
|
|
80
89
|
|
|
81
90
|
|
|
91
|
+
def zip_dir_using_bundle_map(
|
|
92
|
+
bundle_map: BundleMap,
|
|
93
|
+
dest_zip: Path,
|
|
94
|
+
mode: Literal["r", "w", "x", "a"] = "w",
|
|
95
|
+
) -> None:
|
|
96
|
+
if not dest_zip.parent.exists():
|
|
97
|
+
SecurePath(dest_zip).parent.mkdir(parents=True)
|
|
98
|
+
|
|
99
|
+
with ZipFile(dest_zip, mode, ZIP_DEFLATED, allowZip64=True) as package_zip:
|
|
100
|
+
cli_console.step(f"Creating: {dest_zip}")
|
|
101
|
+
for src, _ in bundle_map.all_mappings(expand_directories=True):
|
|
102
|
+
if src.is_file():
|
|
103
|
+
log.debug("Adding %s to %s", src, dest_zip)
|
|
104
|
+
package_zip.write(src, arcname=_path_without_top_level_directory(src))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _path_without_top_level_directory(path: Path) -> str:
|
|
108
|
+
path_parts = path.parts
|
|
109
|
+
if len(path_parts) > 1:
|
|
110
|
+
return str(Path(*path_parts[1:]))
|
|
111
|
+
return str(path)
|
|
112
|
+
|
|
113
|
+
|
|
82
114
|
def _to_be_zipped(file: Path) -> bool:
|
|
83
115
|
for pattern in IGNORED_FILES:
|
|
84
116
|
# This has to be a string because of fnmatch
|