snowflake-cli-labs 2.6.1__py3-none-any.whl → 2.7.0__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 (86) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/cli_global_context.py +9 -0
  3. snowflake/cli/api/commands/decorators.py +9 -4
  4. snowflake/cli/api/commands/execution_metadata.py +40 -0
  5. snowflake/cli/api/commands/flags.py +45 -36
  6. snowflake/cli/api/commands/project_initialisation.py +5 -2
  7. snowflake/cli/api/commands/snow_typer.py +20 -9
  8. snowflake/cli/api/config.py +1 -0
  9. snowflake/cli/api/errno.py +27 -0
  10. snowflake/cli/api/feature_flags.py +5 -0
  11. snowflake/cli/api/identifiers.py +20 -3
  12. snowflake/cli/api/output/types.py +9 -0
  13. snowflake/cli/api/project/definition_manager.py +2 -2
  14. snowflake/cli/api/project/project_verification.py +23 -0
  15. snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
  16. snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
  17. snowflake/cli/api/project/schemas/entities/common.py +85 -0
  18. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  19. snowflake/cli/api/project/schemas/project_definition.py +114 -22
  20. snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
  21. snowflake/cli/api/project/schemas/template.py +77 -0
  22. snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
  23. snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
  24. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  25. snowflake/cli/api/rendering/project_templates.py +97 -0
  26. snowflake/cli/api/rendering/sql_templates.py +56 -0
  27. snowflake/cli/api/sql_execution.py +40 -1
  28. snowflake/cli/api/utils/definition_rendering.py +8 -5
  29. snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
  30. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
  31. snowflake/cli/app/loggers.py +3 -1
  32. snowflake/cli/app/printing.py +17 -7
  33. snowflake/cli/app/snow_connector.py +9 -1
  34. snowflake/cli/app/telemetry.py +41 -2
  35. snowflake/cli/plugins/connection/commands.py +13 -3
  36. snowflake/cli/plugins/connection/util.py +73 -18
  37. snowflake/cli/plugins/cortex/commands.py +2 -1
  38. snowflake/cli/plugins/git/commands.py +20 -4
  39. snowflake/cli/plugins/git/manager.py +44 -20
  40. snowflake/cli/plugins/init/__init__.py +13 -0
  41. snowflake/cli/plugins/init/commands.py +242 -0
  42. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  43. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
  44. snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
  45. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
  46. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  47. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  48. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
  49. snowflake/cli/plugins/nativeapp/commands.py +100 -6
  50. snowflake/cli/plugins/nativeapp/constants.py +0 -6
  51. snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
  52. snowflake/cli/plugins/nativeapp/init.py +1 -1
  53. snowflake/cli/plugins/nativeapp/manager.py +114 -39
  54. snowflake/cli/plugins/nativeapp/project_model.py +8 -4
  55. snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
  56. snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
  57. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
  58. snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
  59. snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
  60. snowflake/cli/plugins/snowpark/commands.py +34 -26
  61. snowflake/cli/plugins/snowpark/common.py +88 -27
  62. snowflake/cli/plugins/snowpark/manager.py +16 -5
  63. snowflake/cli/plugins/snowpark/models.py +6 -0
  64. snowflake/cli/plugins/sql/commands.py +3 -5
  65. snowflake/cli/plugins/sql/manager.py +1 -1
  66. snowflake/cli/plugins/stage/commands.py +2 -2
  67. snowflake/cli/plugins/stage/diff.py +27 -64
  68. snowflake/cli/plugins/stage/manager.py +290 -86
  69. snowflake/cli/plugins/stage/md5.py +160 -0
  70. snowflake/cli/plugins/streamlit/commands.py +20 -6
  71. snowflake/cli/plugins/streamlit/manager.py +46 -32
  72. snowflake/cli/plugins/workspace/__init__.py +13 -0
  73. snowflake/cli/plugins/workspace/commands.py +35 -0
  74. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  75. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
  76. snowflake/cli/templates/default_snowpark/app/common.py +0 -15
  77. snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
  78. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
  79. snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
  80. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
  81. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
  82. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/METADATA +7 -6
  83. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/RECORD +86 -65
  84. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/WHEEL +0 -0
  85. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/entry_points.txt +0 -0
  86. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,13 +12,50 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from pathlib import Path
16
18
  from textwrap import dedent
19
+ from typing import List
17
20
 
18
- from snowflake.cli.plugins.stage.manager import StageManager, StagePathParts
21
+ from snowflake.cli.plugins.stage.manager import (
22
+ USER_STAGE_PREFIX,
23
+ StageManager,
24
+ StagePathParts,
25
+ UserStagePathParts,
26
+ )
19
27
  from snowflake.connector.cursor import SnowflakeCursor
20
28
 
21
29
 
30
+ class GitStagePathParts(StagePathParts):
31
+ def __init__(self, stage_path: str):
32
+ self.stage = GitManager.get_stage_from_path(stage_path)
33
+ stage_path_parts = Path(stage_path).parts
34
+ git_repo_name = stage_path_parts[0].split(".")[-1]
35
+ if git_repo_name.startswith("@"):
36
+ git_repo_name = git_repo_name[1:]
37
+ self.stage_name = "/".join([git_repo_name, *stage_path_parts[1:3], ""])
38
+ self.directory = "/".join(stage_path_parts[3:])
39
+ self.is_directory = True if stage_path.endswith("/") else False
40
+
41
+ @property
42
+ def path(self) -> str:
43
+ return (
44
+ f"{self.stage_name}{self.directory}"
45
+ if self.stage_name.endswith("/")
46
+ else f"{self.stage_name}/{self.directory}"
47
+ )
48
+
49
+ def add_stage_prefix(self, file_path: str) -> str:
50
+ stage = Path(self.stage).parts[0]
51
+ file_path_without_prefix = Path(file_path).parts[1:]
52
+ return f"{stage}/{'/'.join(file_path_without_prefix)}"
53
+
54
+ def get_directory_from_file_path(self, file_path: str) -> List[str]:
55
+ stage_path_length = len(Path(self.directory).parts)
56
+ return list(Path(file_path).parts[3 + stage_path_length : -1])
57
+
58
+
22
59
  class GitManager(StageManager):
23
60
  def show_branches(self, repo_name: str, like: str) -> SnowflakeCursor:
24
61
  return self._execute_query(f"show git branches like '{like}' in {repo_name}")
@@ -51,22 +88,9 @@ class GitManager(StageManager):
51
88
  """
52
89
  return f"{'/'.join(Path(path).parts[0:3])}/"
53
90
 
54
- def _split_stage_path(self, stage_path: str) -> StagePathParts:
55
- """
56
- Splits Git repository path `@repo/branch/main/dir`
57
- stage -> @repo/branch/main/
58
- stage_name -> repo/branch/main/
59
- directory -> dir
60
- For Git repository with fully qualified name `@db.schema.repo/branch/main/dir`
61
- stage -> @db.schema.repo/branch/main/
62
- stage_name -> repo/branch/main/
63
- directory -> dir
64
- """
65
- stage = self.get_stage_from_path(stage_path)
66
- stage_path_parts = Path(stage_path).parts
67
- git_repo_name = stage_path_parts[0].split(".")[-1]
68
- if git_repo_name.startswith("@"):
69
- git_repo_name = git_repo_name[1:]
70
- stage_name = "/".join([git_repo_name, *stage_path_parts[1:3], ""])
71
- directory = "/".join(stage_path_parts[3:])
72
- return StagePathParts(stage, stage_name, directory)
91
+ @staticmethod
92
+ def _stage_path_part_factory(stage_path: str) -> StagePathParts:
93
+ stage_path = StageManager.get_standard_stage_prefix(stage_path)
94
+ if stage_path.startswith(USER_STAGE_PREFIX):
95
+ return UserStagePathParts(stage_path)
96
+ return GitStagePathParts(stage_path)
@@ -0,0 +1,13 @@
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.
@@ -0,0 +1,242 @@
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 logging
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ import typer
21
+ import yaml
22
+ from click import ClickException
23
+ from snowflake.cli.api.commands.flags import (
24
+ NoInteractiveOption,
25
+ parse_key_value_variables,
26
+ variables_option,
27
+ )
28
+ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
29
+ from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
30
+ from snowflake.cli.api.exceptions import InvalidTemplate
31
+ from snowflake.cli.api.output.types import (
32
+ CommandResult,
33
+ MessageResult,
34
+ )
35
+ from snowflake.cli.api.project.schemas.template import Template, TemplateVariable
36
+ from snowflake.cli.api.rendering.project_templates import render_template_files
37
+ from snowflake.cli.api.secure_path import SecurePath
38
+
39
+ # simple Typer with defaults because it won't become a command group as it contains only one command
40
+ app = SnowTyperFactory()
41
+
42
+
43
+ DEFAULT_SOURCE = "https://github.com/snowflakedb/snowflake-cli-templates"
44
+
45
+ log = logging.getLogger(__name__)
46
+
47
+
48
+ def _path_argument_callback(path: str) -> str:
49
+ if SecurePath(path).exists():
50
+ raise ClickException(
51
+ f"The directory {path} already exists. Please specify a different path for the project."
52
+ )
53
+ return path
54
+
55
+
56
+ PathArgument = typer.Argument(
57
+ ...,
58
+ help="Directory to be initialized with the project. This directory must not already exist",
59
+ show_default=False,
60
+ callback=_path_argument_callback,
61
+ )
62
+ TemplateOption = typer.Option(
63
+ None,
64
+ "--template",
65
+ help="which template (subdirectory of --template-source) should be used. If not provided,"
66
+ " whole source will be used as the template.",
67
+ show_default=False,
68
+ )
69
+ SourceOption = typer.Option(
70
+ default=DEFAULT_SOURCE,
71
+ help=f"local path to template directory or URL to git repository with templates.",
72
+ )
73
+ VariablesOption = variables_option(
74
+ "String in `key=value` format. Provided variables will not be prompted for."
75
+ )
76
+
77
+ TEMPLATE_METADATA_FILE_NAME = "template.yml"
78
+
79
+
80
+ def _fetch_local_template(
81
+ template_source: SecurePath, path: Optional[str], destination: SecurePath
82
+ ) -> SecurePath:
83
+ """Copies local template to [dest] and returns path to the template root.
84
+ Ends with an error of the template does not exist."""
85
+
86
+ template_source.assert_exists()
87
+ template_origin = template_source / path if path else template_source
88
+ log.info("Copying local template from %s", template_origin.path)
89
+ if not template_origin.exists():
90
+ raise ClickException(
91
+ f"Template '{path}' cannot be found under {template_source}"
92
+ )
93
+
94
+ template_origin.copy(destination.path)
95
+ return destination / template_origin.name
96
+
97
+
98
+ def _fetch_remote_template(
99
+ url: str, path: Optional[str], destination: SecurePath
100
+ ) -> SecurePath:
101
+ """Downloads remote repository template to [dest],
102
+ and returns path to the template root.
103
+ Ends with an error of the template does not exist."""
104
+ from git import GitCommandError
105
+ from git import rmtree as git_rmtree
106
+
107
+ # TODO: during nativeapp refactor get rid of this dependency
108
+ from snowflake.cli.plugins.nativeapp.utils import shallow_git_clone
109
+
110
+ log.info("Downloading remote template from %s", url)
111
+ try:
112
+ shallow_git_clone(url, to_path=destination.path)
113
+ except GitCommandError as err:
114
+ import re
115
+
116
+ if re.search("fatal: repository '.*' not found", err.stderr):
117
+ raise ClickException(f"Repository '{url}' does not exist")
118
+ raise
119
+
120
+ if path:
121
+ # template is a subdirectoruy of the repository
122
+ template_root = destination / path
123
+ else:
124
+ # template is a whole repository
125
+ # removing .git directory not to copy it to the template
126
+ template_root = destination
127
+ git_rmtree((template_root / ".git").path)
128
+ if not template_root.exists():
129
+ raise ClickException(f"Template '{path}' cannot be found under {url}")
130
+
131
+ return template_root
132
+
133
+
134
+ def _read_template_metadata(template_root: SecurePath) -> Template:
135
+ """Parse template.yml file."""
136
+ template_metadata_path = template_root / TEMPLATE_METADATA_FILE_NAME
137
+ log.debug("Reading template metadata from %s", template_metadata_path.path)
138
+ if not template_metadata_path.exists():
139
+ raise InvalidTemplate(
140
+ f"Template does not have {TEMPLATE_METADATA_FILE_NAME} file."
141
+ )
142
+ with template_metadata_path.open(read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as fd:
143
+ yaml_contents = yaml.safe_load(fd) or {}
144
+ return Template(template_root, **yaml_contents)
145
+
146
+
147
+ def _remove_template_metadata_file(template_root: SecurePath) -> None:
148
+ (template_root / TEMPLATE_METADATA_FILE_NAME).unlink()
149
+
150
+
151
+ def _determine_variable_values(
152
+ variables_metadata: List[TemplateVariable],
153
+ variables_from_flags: Dict[str, Any],
154
+ no_interactive: bool,
155
+ ) -> Dict[str, Any]:
156
+ """
157
+ Prompt user for values not provided in [variables_from_flags].
158
+ If [no_interactive] is True, fill not provided variables with their default values.
159
+ """
160
+ result = {}
161
+
162
+ log.debug(
163
+ "Resolving values of variables: %s",
164
+ ", ".join(v.name for v in variables_metadata),
165
+ )
166
+ for variable in variables_metadata:
167
+ if variable.name in variables_from_flags:
168
+ value = variable.python_type(variables_from_flags[variable.name])
169
+ else:
170
+ value = variable.prompt_user_for_value(no_interactive)
171
+
172
+ result[variable.name] = value
173
+
174
+ return result
175
+
176
+
177
+ def _validate_cli_version(required_version: str) -> None:
178
+ from packaging.version import parse
179
+ from snowflake.cli.__about__ import VERSION
180
+
181
+ if parse(required_version) > parse(VERSION):
182
+ raise ClickException(
183
+ f"Snowflake CLI version ({VERSION}) is too low - minimum version required"
184
+ f" by template is {required_version}. Please upgrade before continuing."
185
+ )
186
+
187
+
188
+ @app.command(no_args_is_help=True)
189
+ def init(
190
+ path: str = PathArgument,
191
+ template: Optional[str] = TemplateOption,
192
+ template_source: Optional[str] = SourceOption,
193
+ variables: Optional[List[str]] = VariablesOption,
194
+ no_interactive: bool = NoInteractiveOption,
195
+ **options,
196
+ ) -> CommandResult:
197
+ """
198
+ Creates project directory from template.
199
+ """
200
+ variables_from_flags = {
201
+ v.key: v.value for v in parse_key_value_variables(variables)
202
+ }
203
+ is_remote = any(
204
+ template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
205
+ )
206
+
207
+ # copy/download template into tmpdir, so it is going to be removed in case command ends with an error
208
+ with SecurePath.temporary_directory() as tmpdir:
209
+ if is_remote:
210
+ template_root = _fetch_remote_template(
211
+ url=template_source, path=template, destination=tmpdir # type: ignore
212
+ )
213
+ else:
214
+ template_root = _fetch_local_template(
215
+ template_source=SecurePath(template_source),
216
+ path=template,
217
+ destination=tmpdir,
218
+ )
219
+
220
+ template_metadata = _read_template_metadata(template_root)
221
+ if template_metadata.minimum_cli_version:
222
+ _validate_cli_version(template_metadata.minimum_cli_version)
223
+
224
+ variable_values = _determine_variable_values(
225
+ variables_metadata=template_metadata.variables,
226
+ variables_from_flags=variables_from_flags,
227
+ no_interactive=no_interactive,
228
+ )
229
+ variable_values["project_dir_name"] = SecurePath(path).name
230
+ log.debug(
231
+ "Rendering template files: %s", ", ".join(template_metadata.files_to_render)
232
+ )
233
+ render_template_files(
234
+ template_root=template_root,
235
+ files_to_render=template_metadata.files_to_render,
236
+ data=variable_values,
237
+ )
238
+ _remove_template_metadata_file(template_root)
239
+ SecurePath(path).parent.mkdir(exist_ok=True, parents=True)
240
+ template_root.copy(path)
241
+
242
+ return MessageResult(f"Initialized the new project in {path}")
@@ -0,0 +1,30 @@
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 snowflake.cli.api.plugins.command import (
16
+ SNOWCLI_ROOT_COMMAND_PATH,
17
+ CommandSpec,
18
+ CommandType,
19
+ plugin_hook_impl,
20
+ )
21
+ from snowflake.cli.plugins.init import commands
22
+
23
+
24
+ @plugin_hook_impl
25
+ def command_spec():
26
+ return CommandSpec(
27
+ parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
28
+ command_type=CommandType.SINGLE_COMMAND,
29
+ typer_instance=commands.app.create_instance(),
30
+ )
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from abc import ABC, abstractmethod
18
+ from pathlib import Path
18
19
  from typing import Optional
19
20
 
20
21
  from click import ClickException
@@ -34,6 +35,42 @@ class UnsupportedArtifactProcessorError(ClickException):
34
35
  )
35
36
 
36
37
 
38
+ def is_python_file_artifact(src: Path, _: Path):
39
+ """Determines whether the provided source path is an existing python file."""
40
+ return src.is_file() and src.suffix == ".py"
41
+
42
+
43
+ class ProjectFileContextManager:
44
+ """
45
+ A context manager that encapsulates the logic required to update a project file
46
+ in processor logic. The processor can use this manager to gain access to the contents
47
+ of a file, and optionally provide replacement contents. If it does, the file is
48
+ correctly modified in the deploy root directory to reflect the new contents.
49
+ """
50
+
51
+ def __init__(self, path: Path):
52
+ self.path = path
53
+ self._contents = None
54
+ self.edited_contents = None
55
+
56
+ @property
57
+ def contents(self):
58
+ return self._contents
59
+
60
+ def __enter__(self):
61
+ self._contents = self.path.read_text(encoding="utf-8")
62
+
63
+ return self
64
+
65
+ def __exit__(self, exc_type, exc_val, exc_tb):
66
+ if self.edited_contents is not None:
67
+ if self.path.is_symlink():
68
+ # if the file is a symlink, make sure we don't overwrite the original
69
+ self.path.unlink()
70
+
71
+ self.path.write_text(self.edited_contents, encoding="utf-8")
72
+
73
+
37
74
  class ArtifactProcessor(ABC):
38
75
  def __init__(
39
76
  self,
@@ -49,3 +86,6 @@ class ArtifactProcessor(ABC):
49
86
  **kwargs,
50
87
  ) -> None:
51
88
  pass
89
+
90
+ def edit_file(self, path: Path):
91
+ return ProjectFileContextManager(path)
@@ -24,12 +24,22 @@ from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
24
24
  ArtifactProcessor,
25
25
  UnsupportedArtifactProcessorError,
26
26
  )
27
+ from snowflake.cli.plugins.nativeapp.codegen.setup.native_app_setup_processor import (
28
+ NativeAppSetupProcessor,
29
+ )
27
30
  from snowflake.cli.plugins.nativeapp.codegen.snowpark.python_processor import (
28
31
  SnowparkAnnotationProcessor,
29
32
  )
33
+ from snowflake.cli.plugins.nativeapp.feature_flags import FeatureFlag
30
34
  from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
31
35
 
32
36
  SNOWPARK_PROCESSOR = "snowpark"
37
+ NA_SETUP_PROCESSOR = "native-app-setup"
38
+
39
+ _REGISTERED_PROCESSORS_BY_NAME = {
40
+ SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor,
41
+ NA_SETUP_PROCESSOR: NativeAppSetupProcessor,
42
+ }
33
43
 
34
44
 
35
45
  class NativeAppCompiler:
@@ -54,28 +64,31 @@ class NativeAppCompiler:
54
64
  Go through every artifact object in the project definition of a native app, and execute processors in order of specification for each of the artifact object.
55
65
  May have side-effects on the filesystem by either directly editing source files or the deploy root.
56
66
  """
57
- should_proceed = False
58
- for artifact in self._na_project.artifacts:
59
- if artifact.processors:
60
- should_proceed = True
61
- break
62
- if not should_proceed:
67
+
68
+ if not self._should_invoke_processors():
63
69
  return
64
70
 
65
71
  with cc.phase("Invoking artifact processors"):
72
+ if self._na_project.generated_root.exists():
73
+ raise ClickException(
74
+ f"Path {self._na_project.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
75
+ )
76
+
66
77
  for artifact in self._na_project.artifacts:
67
78
  for processor in artifact.processors:
68
- artifact_processor = self._try_create_processor(
69
- processor_mapping=processor,
70
- )
71
- if artifact_processor is None:
72
- raise UnsupportedArtifactProcessorError(
73
- processor_name=processor.name
74
- )
75
- else:
76
- artifact_processor.process(
77
- artifact_to_process=artifact, processor_mapping=processor
79
+ if self._is_enabled(processor):
80
+ artifact_processor = self._try_create_processor(
81
+ processor_mapping=processor,
78
82
  )
83
+ if artifact_processor is None:
84
+ raise UnsupportedArtifactProcessorError(
85
+ processor_name=processor.name
86
+ )
87
+ else:
88
+ artifact_processor.process(
89
+ artifact_to_process=artifact,
90
+ processor_mapping=processor,
91
+ )
79
92
 
80
93
  def _try_create_processor(
81
94
  self,
@@ -86,15 +99,32 @@ class NativeAppCompiler:
86
99
  Fetch processor object if one already exists in the cached_processors dictionary.
87
100
  Else, initialize a new object to return, and add it to the cached_processors dictionary.
88
101
  """
89
- if processor_mapping.name.lower() == SNOWPARK_PROCESSOR:
90
- curr_processor = self.cached_processors.get(SNOWPARK_PROCESSOR, None)
91
- if curr_processor is not None:
92
- return curr_processor
93
- else:
94
- curr_processor = SnowparkAnnotationProcessor(
95
- na_project=self._na_project,
96
- )
97
- self.cached_processors[SNOWPARK_PROCESSOR] = curr_processor
98
- return curr_processor
99
- else:
102
+ processor_name = processor_mapping.name.lower()
103
+ current_processor = self.cached_processors.get(processor_name)
104
+
105
+ if current_processor is not None:
106
+ return current_processor
107
+
108
+ processor_factory = _REGISTERED_PROCESSORS_BY_NAME.get(processor_name)
109
+ if processor_factory is None:
110
+ # No registered processor with the specified name
100
111
  return None
112
+
113
+ current_processor = processor_factory(
114
+ na_project=self._na_project,
115
+ )
116
+ self.cached_processors[processor_name] = current_processor
117
+
118
+ return current_processor
119
+
120
+ def _should_invoke_processors(self):
121
+ for artifact in self._na_project.artifacts:
122
+ for processor in artifact.processors:
123
+ if self._is_enabled(processor):
124
+ return True
125
+ return False
126
+
127
+ def _is_enabled(self, processor: ProcessorMapping) -> bool:
128
+ if processor.name.lower() == NA_SETUP_PROCESSOR:
129
+ return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled()
130
+ return True