diff --git a/src/snowflake/cli/plugins/nativeapp/bundle_context.py b/src/snowflake/cli/plugins/nativeapp/bundle_context.py new file mode 100644 index 0000000000..4a31a0b87f --- /dev/null +++ b/src/snowflake/cli/plugins/nativeapp/bundle_context.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Snowflake Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from pathlib import Path +from typing import ( + List, +) + +from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping + + +@dataclass +class BundleContext: + package_name: str + artifacts: List[PathMapping] + project_root: Path + bundle_root: Path + deploy_root: Path + generated_root: Path diff --git a/src/snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py b/src/snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py index da20ad9608..707fffc3c3 100644 --- a/src/snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +++ b/src/snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py @@ -23,7 +23,7 @@ PathMapping, ProcessorMapping, ) -from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel +from snowflake.cli.plugins.nativeapp.bundle_context import BundleContext class UnsupportedArtifactProcessorError(ClickException): @@ -74,9 +74,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): class ArtifactProcessor(ABC): def __init__( self, - na_project: NativeAppProjectModel, + bundle_ctx: BundleContext, ) -> None: - self._na_project = na_project + self._bundle_ctx = bundle_ctx @abstractmethod def process( diff --git a/src/snowflake/cli/plugins/nativeapp/codegen/compiler.py b/src/snowflake/cli/plugins/nativeapp/codegen/compiler.py index 2bdd869c12..a4a1fa6849 100644 --- a/src/snowflake/cli/plugins/nativeapp/codegen/compiler.py +++ b/src/snowflake/cli/plugins/nativeapp/codegen/compiler.py @@ -20,6 +20,7 @@ from snowflake.cli.api.project.schemas.native_app.path_mapping import ( ProcessorMapping, ) +from snowflake.cli.plugins.nativeapp.bundle_context import BundleContext from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import ( ArtifactProcessor, UnsupportedArtifactProcessorError, @@ -31,7 +32,6 @@ SnowparkAnnotationProcessor, ) from snowflake.cli.plugins.nativeapp.feature_flags import FeatureFlag -from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel SNOWPARK_PROCESSOR = "snowpark" NA_SETUP_PROCESSOR = "native-app-setup" @@ -53,9 +53,9 @@ class NativeAppCompiler: def __init__( self, - na_project: NativeAppProjectModel, + bundle_ctx: BundleContext, ): - self._na_project = na_project + self._bundle_ctx = bundle_ctx # dictionary of all processors created and shared between different artifact objects. self.cached_processors: Dict[str, ArtifactProcessor] = {} @@ -69,12 +69,12 @@ def compile_artifacts(self): return with cc.phase("Invoking artifact processors"): - if self._na_project.generated_root.exists(): + if self._bundle_ctx.generated_root.exists(): raise ClickException( - f"Path {self._na_project.generated_root} already exists. Please choose a different name for your generated directory in the project definition file." + f"Path {self._bundle_ctx.generated_root} already exists. Please choose a different name for your generated directory in the project definition file." ) - for artifact in self._na_project.artifacts: + for artifact in self._bundle_ctx.artifacts: for processor in artifact.processors: if self._is_enabled(processor): artifact_processor = self._try_create_processor( @@ -110,15 +110,13 @@ def _try_create_processor( # No registered processor with the specified name return None - current_processor = processor_factory( - na_project=self._na_project, - ) + current_processor = processor_factory(self._bundle_ctx) self.cached_processors[processor_name] = current_processor return current_processor def _should_invoke_processors(self): - for artifact in self._na_project.artifacts: + for artifact in self._bundle_ctx.artifacts: for processor in artifact.processors: if self._is_enabled(processor): return True diff --git a/src/snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py b/src/snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py index b1cfca12d2..4c619b8223 100644 --- a/src/snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +++ b/src/snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py @@ -35,7 +35,6 @@ SandboxEnvBuilder, execute_script_in_sandbox, ) -from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel from snowflake.cli.plugins.stage.diff import to_stage_path DEFAULT_TIMEOUT = 30 @@ -43,11 +42,8 @@ class NativeAppSetupProcessor(ArtifactProcessor): - def __init__( - self, - na_project: NativeAppProjectModel, - ): - super().__init__(na_project=na_project) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def process( self, @@ -59,8 +55,8 @@ def process( Processes a Python setup script and generates the corresponding SQL commands. """ bundle_map = BundleMap( - project_root=self._na_project.project_root, - deploy_root=self._na_project.deploy_root, + project_root=self._bundle_ctx.project_root, + deploy_root=self._bundle_ctx.deploy_root, ) bundle_map.add(artifact_to_process) @@ -73,7 +69,7 @@ def process( absolute=True, expand_directories=True, predicate=is_python_file_artifact ): cc.message( - f"Found Python setup file: {src_file.relative_to(self._na_project.project_root)}" + f"Found Python setup file: {src_file.relative_to(self._bundle_ctx.project_root)}" ) files_to_process.append(src_file) @@ -85,9 +81,9 @@ def _execute_in_sandbox(self, py_files: List[Path]) -> dict: cc.step(f"Processing {file_count} setup file{'s' if file_count > 1 else ''}") env_vars = { - "_SNOWFLAKE_CLI_PROJECT_PATH": str(self._na_project.project_root), + "_SNOWFLAKE_CLI_PROJECT_PATH": str(self._bundle_ctx.project_root), "_SNOWFLAKE_CLI_SETUP_FILES": os.pathsep.join(map(str, py_files)), - "_SNOWFLAKE_CLI_APP_NAME": str(self._na_project.package_name), + "_SNOWFLAKE_CLI_APP_NAME": str(self._bundle_ctx.package_name), "_SNOWFLAKE_CLI_SQL_DEST_DIR": str(self.generated_root), } @@ -95,7 +91,7 @@ def _execute_in_sandbox(self, py_files: List[Path]) -> dict: result = execute_script_in_sandbox( script_source=DRIVER_PATH.read_text(), env_type=ExecutionEnvironmentType.VENV, - cwd=self._na_project.bundle_root, + cwd=self._bundle_ctx.bundle_root, timeout=DEFAULT_TIMEOUT, path=self.sandbox_root, env_vars=env_vars, @@ -123,7 +119,7 @@ def _generate_setup_sql(self, sql_file_mappings: dict) -> None: cc.step("Patching setup script") setup_file_path = find_setup_script_file( - deploy_root=self._na_project.deploy_root + deploy_root=self._bundle_ctx.deploy_root ) with self.edit_file(setup_file_path) as f: new_contents = [f.contents] @@ -132,30 +128,30 @@ def _generate_setup_sql(self, sql_file_mappings: dict) -> None: schemas_file = generated_root / sql_file_mappings["schemas"] new_contents.insert( 0, - f"EXECUTE IMMEDIATE FROM '/{to_stage_path(schemas_file.relative_to(self._na_project.deploy_root))}';", + f"EXECUTE IMMEDIATE FROM '/{to_stage_path(schemas_file.relative_to(self._bundle_ctx.deploy_root))}';", ) if sql_file_mappings["compute_pools"]: compute_pools_file = generated_root / sql_file_mappings["compute_pools"] new_contents.append( - f"EXECUTE IMMEDIATE FROM '/{to_stage_path(compute_pools_file.relative_to(self._na_project.deploy_root))}';" + f"EXECUTE IMMEDIATE FROM '/{to_stage_path(compute_pools_file.relative_to(self._bundle_ctx.deploy_root))}';" ) if sql_file_mappings["services"]: services_file = generated_root / sql_file_mappings["services"] new_contents.append( - f"EXECUTE IMMEDIATE FROM '/{to_stage_path(services_file.relative_to(self._na_project.deploy_root))}';" + f"EXECUTE IMMEDIATE FROM '/{to_stage_path(services_file.relative_to(self._bundle_ctx.deploy_root))}';" ) f.edited_contents = "\n".join(new_contents) @property def sandbox_root(self): - return self._na_project.bundle_root / "setup_py_venv" + return self._bundle_ctx.bundle_root / "setup_py_venv" @property def generated_root(self): - return self._na_project.generated_root / "setup_py" + return self._bundle_ctx.generated_root / "setup_py" def _create_or_update_sandbox(self): sandbox_root = self.sandbox_root @@ -164,7 +160,7 @@ def _create_or_update_sandbox(self): cc.step("Virtual environment found") else: cc.step( - f"Creating virtual environment in {sandbox_root.relative_to(self._na_project.project_root)}" + f"Creating virtual environment in {sandbox_root.relative_to(self._bundle_ctx.project_root)}" ) env_builder.ensure_created() diff --git a/src/snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py b/src/snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py index c25e3715e7..d073d0b9ba 100644 --- a/src/snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +++ b/src/snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py @@ -53,7 +53,6 @@ ExtensionFunctionTypeEnum, NativeAppExtensionFunction, ) -from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel from snowflake.cli.plugins.stage.diff import to_stage_path DEFAULT_TIMEOUT = 30 @@ -163,11 +162,8 @@ class SnowparkAnnotationProcessor(ArtifactProcessor): and generate SQL code for creation of extension functions based on those discovered objects. """ - def __init__( - self, - na_project: NativeAppProjectModel, - ): - super().__init__(na_project=na_project) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def process( self, @@ -181,8 +177,8 @@ def process( """ bundle_map = BundleMap( - project_root=self._na_project.project_root, - deploy_root=self._na_project.deploy_root, + project_root=self._bundle_ctx.project_root, + deploy_root=self._bundle_ctx.deploy_root, ) bundle_map.add(artifact_to_process) @@ -235,7 +231,7 @@ def process( @property def _generated_root(self): - return self._na_project.generated_root / "snowpark" + return self._bundle_ctx.generated_root / "snowpark" def _normalize_imports( self, @@ -315,7 +311,7 @@ def collect_extension_functions( self, bundle_map: BundleMap, processor_mapping: Optional[ProcessorMapping] ) -> Dict[Path, List[NativeAppExtensionFunction]]: kwargs = ( - _determine_virtual_env(self._na_project.project_root, processor_mapping) + _determine_virtual_env(self._bundle_ctx.project_root, processor_mapping) if processor_mapping is not None else {} ) @@ -338,7 +334,7 @@ def collect_extension_functions( ) collected_extension_function_json = _execute_in_sandbox( py_file=str(dest_file.resolve()), - deploy_root=self._na_project.deploy_root, + deploy_root=self._bundle_ctx.deploy_root, kwargs=kwargs, ) @@ -369,7 +365,7 @@ def generate_new_sql_file_name(self, py_file: Path) -> Path: """ Generates a SQL filename for the generated root from the python file, and creates its parent directories. """ - relative_py_file = py_file.relative_to(self._na_project.deploy_root) + relative_py_file = py_file.relative_to(self._bundle_ctx.deploy_root) sql_file = Path(self._generated_root, relative_py_file.with_suffix(".sql")) if sql_file.exists(): cc.warning( diff --git a/src/snowflake/cli/plugins/nativeapp/manager.py b/src/snowflake/cli/plugins/nativeapp/manager.py index d0cfd1ba27..01392cb20e 100644 --- a/src/snowflake/cli/plugins/nativeapp/manager.py +++ b/src/snowflake/cli/plugins/nativeapp/manager.py @@ -350,9 +350,7 @@ def build_bundle(self) -> BundleMap: Populates the local deploy root from artifact sources. """ bundle_map = build_bundle(self.project_root, self.deploy_root, self.artifacts) - compiler = NativeAppCompiler( - na_project=self.na_project, - ) + compiler = NativeAppCompiler(self.na_project.get_bundle_context()) compiler.compile_artifacts() return bundle_map diff --git a/src/snowflake/cli/plugins/nativeapp/project_model.py b/src/snowflake/cli/plugins/nativeapp/project_model.py index e2a35a5cd5..29e007475b 100644 --- a/src/snowflake/cli/plugins/nativeapp/project_model.py +++ b/src/snowflake/cli/plugins/nativeapp/project_model.py @@ -31,6 +31,7 @@ from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping from snowflake.cli.api.project.util import extract_schema, to_identifier from snowflake.cli.plugins.nativeapp.artifacts import resolve_without_follow +from snowflake.cli.plugins.nativeapp.bundle_context import BundleContext from snowflake.connector import DictCursor @@ -185,3 +186,13 @@ def debug_mode(self) -> Optional[bool]: if self.definition.application: return self.definition.application.debug return None + + def get_bundle_context(self) -> BundleContext: + return BundleContext( + package_name=self.package_name, + artifacts=self.artifacts, + project_root=self.project_root, + bundle_root=self.bundle_root, + deploy_root=self.deploy_root, + generated_root=self.generated_root, + ) diff --git a/tests/nativeapp/codegen/snowpark/test_python_processor.py b/tests/nativeapp/codegen/snowpark/test_python_processor.py index 1d3c93619d..4f0a247482 100644 --- a/tests/nativeapp/codegen/snowpark/test_python_processor.py +++ b/tests/nativeapp/codegen/snowpark/test_python_processor.py @@ -353,7 +353,8 @@ def test_process_no_collected_functions( project_definition=native_app_project_instance.native_app, project_root=local_path, ) - SnowparkAnnotationProcessor(na_project=project).process( + processor = SnowparkAnnotationProcessor(project.get_bundle_context()) + processor.process( artifact_to_process=native_app_project_instance.native_app.artifacts[0], processor_mapping=ProcessorMapping(name="SNOWPARK"), write_to_sql=False, # For testing @@ -404,7 +405,8 @@ def test_process_with_collected_functions( project_definition=native_app_project_instance.native_app, project_root=local_path, ) - SnowparkAnnotationProcessor(na_project=project).process( + processor = SnowparkAnnotationProcessor(project.get_bundle_context()) + processor.process( artifact_to_process=native_app_project_instance.native_app.artifacts[0], processor_mapping=processor_mapping, ) @@ -463,7 +465,8 @@ def test_package_normalization( project_definition=native_app_project_instance.native_app, project_root=local_path, ) - SnowparkAnnotationProcessor(na_project=project).process( + processor = SnowparkAnnotationProcessor(project.get_bundle_context()) + processor.process( artifact_to_process=native_app_project_instance.native_app.artifacts[0], processor_mapping=processor_mapping, ) diff --git a/tests/nativeapp/codegen/test_compiler.py b/tests/nativeapp/codegen/test_compiler.py index 4769c8c7ce..225db84786 100644 --- a/tests/nativeapp/codegen/test_compiler.py +++ b/tests/nativeapp/codegen/test_compiler.py @@ -58,9 +58,8 @@ def test_proj_def(): @pytest.fixture() def test_compiler(test_proj_def): - return NativeAppCompiler( - na_project=create_native_app_project_model(test_proj_def.native_app) - ) + na_project = create_native_app_project_model(test_proj_def.native_app) + return NativeAppCompiler(na_project.get_bundle_context()) def test_try_create_processor_returns_none(test_proj_def, test_compiler): @@ -92,7 +91,7 @@ def test_find_and_execute_processors_exception(test_proj_def, test_compiler): app_pkg = create_native_app_project_model( project_definition=test_proj_def.native_app ) - test_compiler = NativeAppCompiler(na_project=app_pkg) + test_compiler = NativeAppCompiler(app_pkg.get_bundle_context()) with pytest.raises(UnsupportedArtifactProcessorError): test_compiler.compile_artifacts() diff --git a/tests/nativeapp/test_artifacts.py b/tests/nativeapp/test_artifacts.py index 3d8b71fd01..6f66d5cb70 100644 --- a/tests/nativeapp/test_artifacts.py +++ b/tests/nativeapp/test_artifacts.py @@ -34,7 +34,10 @@ symlink_or_copy, ) -from tests.nativeapp.utils import assert_dir_snapshot, touch +from tests.nativeapp.utils import ( + assert_dir_snapshot, + touch, +) from tests.testing_utils.files_and_dirs import pushd, temp_local_dir from tests_common import IS_WINDOWS diff --git a/tests/nativeapp/test_project_model.py b/tests/nativeapp/test_project_model.py index 91cea38ceb..19b91635ab 100644 --- a/tests/nativeapp/test_project_model.py +++ b/tests/nativeapp/test_project_model.py @@ -22,12 +22,13 @@ import pytest import yaml -from snowflake.cli.api.project.definition import load_project +from snowflake.cli.api.project.definition import default_app_package, load_project from snowflake.cli.api.project.schemas.native_app.application import SqlScriptHookType from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping from snowflake.cli.api.project.schemas.project_definition import ( build_project_definition, ) +from snowflake.cli.plugins.nativeapp.bundle_context import BundleContext from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel CURRENT_ROLE = "current_role" @@ -173,3 +174,29 @@ def test_project_model_falls_back_to_current_role( assert project.app_role == CURRENT_ROLE assert project.package_role == CURRENT_ROLE + + +@pytest.mark.parametrize("project_definition_files", ["minimal"], indirect=True) +def test_bundle_context_from_project_model(project_definition_files: List[Path]): + project_defn = load_project(project_definition_files).project_definition + project_dir = Path().resolve() + project = NativeAppProjectModel( + project_definition=project_defn.native_app, + project_root=project_dir, + ) + + actual_bundle_ctx = project.get_bundle_context() + + expected_bundle_ctx = BundleContext( + package_name=default_app_package("minimal"), + artifacts=[ + PathMapping(src="setup.sql", dest=None), + PathMapping(src="README.md", dest=None), + ], + project_root=project_dir, + bundle_root=Path(project_dir / "output" / "bundle"), + deploy_root=Path(project_dir / "output" / "deploy"), + generated_root=Path(project_dir / "output" / "deploy" / "__generated"), + ) + + assert actual_bundle_ctx == expected_bundle_ctx