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.
Files changed (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_app/constants.py +4 -0
  6. snowflake/cli/_app/snow_connector.py +12 -0
  7. snowflake/cli/_app/telemetry.py +10 -3
  8. snowflake/cli/_plugins/connection/util.py +12 -19
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +207 -1
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
  19. snowflake/cli/_plugins/nativeapp/commands.py +113 -21
  20. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
  23. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  24. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  25. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  26. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
  27. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  28. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
  29. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  30. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  31. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
  32. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
  35. snowflake/cli/_plugins/notebook/commands.py +55 -2
  36. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  37. snowflake/cli/_plugins/notebook/manager.py +7 -5
  38. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  39. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  40. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  41. snowflake/cli/_plugins/notebook/types.py +3 -0
  42. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  43. snowflake/cli/_plugins/snowpark/common.py +47 -2
  44. snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
  45. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  46. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  47. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  48. snowflake/cli/_plugins/spcs/common.py +129 -0
  49. snowflake/cli/_plugins/spcs/services/commands.py +131 -14
  50. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  51. snowflake/cli/_plugins/stage/commands.py +2 -1
  52. snowflake/cli/_plugins/stage/diff.py +60 -39
  53. snowflake/cli/_plugins/stage/manager.py +34 -13
  54. snowflake/cli/_plugins/stage/utils.py +1 -1
  55. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  56. snowflake/cli/_plugins/streamlit/manager.py +70 -22
  57. snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
  58. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  59. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  60. snowflake/cli/_plugins/workspace/commands.py +6 -5
  61. snowflake/cli/_plugins/workspace/manager.py +9 -5
  62. snowflake/cli/api/artifacts/__init__.py +13 -0
  63. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  64. snowflake/cli/api/artifacts/common.py +78 -0
  65. snowflake/cli/api/artifacts/utils.py +82 -0
  66. snowflake/cli/api/cli_global_context.py +36 -2
  67. snowflake/cli/api/commands/flags.py +10 -4
  68. snowflake/cli/api/commands/utils.py +28 -2
  69. snowflake/cli/api/config.py +6 -2
  70. snowflake/cli/api/connections.py +12 -1
  71. snowflake/cli/api/constants.py +10 -1
  72. snowflake/cli/api/entities/common.py +81 -14
  73. snowflake/cli/api/entities/resolver.py +160 -0
  74. snowflake/cli/api/entities/utils.py +65 -23
  75. snowflake/cli/api/errno.py +63 -3
  76. snowflake/cli/api/feature_flags.py +19 -4
  77. snowflake/cli/api/metrics.py +21 -27
  78. snowflake/cli/api/project/definition_conversion.py +4 -4
  79. snowflake/cli/api/project/project_paths.py +28 -0
  80. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  81. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  82. snowflake/cli/api/project/schemas/project_definition.py +54 -6
  83. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  84. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  85. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  86. snowflake/cli/api/project/util.py +45 -0
  87. snowflake/cli/api/secure_path.py +6 -0
  88. snowflake/cli/api/sql_execution.py +5 -1
  89. snowflake/cli/api/stage_path.py +7 -2
  90. snowflake/cli/api/utils/graph.py +3 -0
  91. snowflake/cli/api/utils/path_utils.py +24 -0
  92. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
  93. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
  94. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
  95. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  96. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,241 @@
1
- from typing import Generic, TypeVar
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
- pass
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
- pass
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
- pass
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
- from pathlib import Path
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
- EntityModelBase,
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 PathMapping(UpdatableModel):
35
- class Config:
36
- frozen = True
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 artefact in artifacts:
69
- if isinstance(artefact, PathMapping):
70
- _artifacts.append(artefact)
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=artefact))
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 represents allows you to manage files paths related to given project.
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 get_artefact_dto(self, artifact_path: PathMapping) -> Artefact:
41
- return Artefact(
42
- dest=artifact_path.dest,
43
- path=self.path_relative_to_root(artifact_path.src),
44
- )
45
-
46
- def get_dependencies_artefact(self) -> Artefact:
47
- return Artefact(dest=None, path=self.dependencies)
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 dependencies(self) -> Path:
61
- return self.path_relative_to_root(Path("dependencies.zip"))
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 Artefact:
66
- """Helper for getting paths related to given artefact."""
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 _artefact_name(self) -> str:
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 artefact path. Directories are mapped to corresponding .zip files.
213
+ Returns post-build artifact path. Directories are mapped to corresponding .zip files.
81
214
  """
82
- return self.path.parent / self._artefact_name
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 artefact should be uploaded.
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._artefact_name
232
+ return self.upload_path(stage) + self._artifact_name
100
233
 
101
234
  def build(self) -> None:
102
- """Build the artefact. Applies only to directories. Files are untouched."""
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 isinstance(source, Path):
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