diff --git a/src/snowflake/cli/_plugins/nativeapp/commands.py b/src/snowflake/cli/_plugins/nativeapp/commands.py index 333aeba32d..ef737a0561 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -222,7 +222,7 @@ def app_open( @app.command("teardown", requires_connection=True) @with_project_definition() -# This command doesn't use @nativeapp_definition_v2_to_v1 because it needs to +# This command doesn't use @single_app_and_package because it needs to # be aware of PDFv2 definitions that have multiple apps created from the same package, # which all need to be torn down. def app_teardown( @@ -233,7 +233,7 @@ def app_teardown( show_default=False, ), interactive: bool = InteractiveOption, - # Same as the param auto-added by @nativeapp_definition_v2_to_v1 + # Same as the param auto-added by @single_app_and_package package_entity_id: Optional[str] = typer.Option( default="", help="The ID of the package entity on which to operate when definition_version is 2 or higher.", diff --git a/src/snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py b/src/snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py index beb59963ae..8b951383d2 100644 --- a/src/snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +++ b/src/snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py @@ -248,39 +248,6 @@ def find_entity( return entity -def nativeapp_definition_v2_to_v1(*, app_required: bool = False): - """ - A command decorator that attempts to automatically convert a native app project from - definition v2 to v1.1. Assumes with_project_definition() has already been called. - The definition object in CliGlobalContext will be replaced with the converted object. - Exactly one application package entity type is expected, and up to one application - entity type is expected. - """ - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - original_pdf: Optional[DefinitionV20] = get_cli_context().project_definition - if not original_pdf: - raise ValueError( - "Project definition could not be found. The nativeapp_definition_v2_to_v1 command decorator assumes with_project_definition() was called before it." - ) - if original_pdf.definition_version == "2": - package_entity_id = kwargs.get("package_entity_id", "") - app_entity_id = kwargs.get("app_entity_id", "") - pdfv1 = _pdf_v2_to_v1( - original_pdf, package_entity_id, app_entity_id, app_required - ) - get_cli_context_manager().override_project_definition = pdfv1 - return func(*args, **kwargs) - - return _options_decorator_factory( - wrapper, additional_options=APP_AND_PACKAGE_OPTIONS - ) - - return decorator - - def single_app_and_package(*, app_required: bool = False): """ A command decorator that attempts to extract a single application package and up to one diff --git a/src/snowflake/cli/_plugins/nativeapp/version/commands.py b/src/snowflake/cli/_plugins/nativeapp/version/commands.py index 7f3a909633..cd47791926 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/commands.py @@ -20,21 +20,17 @@ import typer from click import MissingParameter from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption -from snowflake.cli._plugins.nativeapp.run_processor import NativeAppRunProcessor from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( - nativeapp_definition_v2_to_v1, -) -from snowflake.cli._plugins.nativeapp.version.version_processor import ( - NativeAppVersionCreateProcessor, - NativeAppVersionDropProcessor, + single_app_and_package, ) +from snowflake.cli._plugins.workspace.manager import WorkspaceManager from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.commands.decorators import ( with_project_definition, ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory +from snowflake.cli.api.entities.common import EntityActions from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult -from snowflake.cli.api.project.project_verification import assert_project_type app = SnowTyperFactory( name="version", @@ -46,7 +42,7 @@ @app.command(requires_connection=True) @with_project_definition() -@nativeapp_definition_v2_to_v1() +@single_app_and_package() def create( version: Optional[str] = typer.Argument( None, @@ -71,18 +67,18 @@ def create( """ Adds a new patch to the provided version defined in your application package. If the version does not exist, creates a version with patch 0. """ - - assert_project_type("native_app") - if version is None and patch is not None: raise MissingParameter("Cannot provide a patch without version!") cli_context = get_cli_context() - processor = NativeAppVersionCreateProcessor( - project_definition=cli_context.project_definition.native_app, + ws = WorkspaceManager( + project_definition=cli_context.project_definition, project_root=cli_context.project_root, ) - processor.process( + package_id = options["package_entity_id"] + ws.perform_action( + package_id, + EntityActions.VERSION_CREATE, version=version, patch=patch, force=force, @@ -94,28 +90,29 @@ def create( @app.command("list", requires_connection=True) @with_project_definition() -@nativeapp_definition_v2_to_v1() +@single_app_and_package() def version_list( **options, ) -> CommandResult: """ Lists all versions defined in an application package. """ - - assert_project_type("native_app") - cli_context = get_cli_context() - processor = NativeAppRunProcessor( - project_definition=cli_context.project_definition.native_app, + ws = WorkspaceManager( + project_definition=cli_context.project_definition, project_root=cli_context.project_root, ) - cursor = processor.get_all_existing_versions() + package_id = options["package_entity_id"] + cursor = ws.perform_action( + package_id, + EntityActions.VERSION_LIST, + ) return QueryResult(cursor) @app.command(requires_connection=True) @with_project_definition() -@nativeapp_definition_v2_to_v1() +@single_app_and_package() def drop( version: Optional[str] = typer.Argument( None, @@ -129,13 +126,17 @@ def drop( Drops a version defined in your application package. Versions can either be passed in as an argument to the command or read from the `manifest.yml` file. Dropping patches is not allowed. """ - - assert_project_type("native_app") - cli_context = get_cli_context() - processor = NativeAppVersionDropProcessor( - project_definition=cli_context.project_definition.native_app, + ws = WorkspaceManager( + project_definition=cli_context.project_definition, project_root=cli_context.project_root, ) - processor.process(version, force, interactive) + package_id = options["package_entity_id"] + ws.perform_action( + package_id, + EntityActions.VERSION_DROP, + version=version, + interactive=interactive, + force=force, + ) return MessageResult(f"Version drop is now complete.") diff --git a/tests/nativeapp/test_v2_to_v1.py b/tests/nativeapp/test_v2_to_v1.py deleted file mode 100644 index 45bea0e61c..0000000000 --- a/tests/nativeapp/test_v2_to_v1.py +++ /dev/null @@ -1,431 +0,0 @@ -# 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 unittest import mock - -import pytest -from click import ClickException -from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( - _pdf_v2_to_v1, - nativeapp_definition_v2_to_v1, -) -from snowflake.cli.api.cli_global_context import ( - get_cli_context, - get_cli_context_manager, -) -from snowflake.cli.api.project.schemas.project_definition import ( - DefinitionV11, - DefinitionV20, -) -from snowflake.cli.api.utils.definition_rendering import render_definition_template - - -def package_v2(entity_id: str): - return { - entity_id: { - "type": "application package", - "identifier": entity_id, - "artifacts": [{"src": "app/*", "dest": "./"}], - "manifest": "app/manifest.yml", - "stage": "app.stage", - "bundle_root": "bundle_root_path", - "generated_root": "generated_root_path", - "deploy_root": "deploy_root_path", - "scratch_stage": "scratch_stage_path", - "meta": { - "role": "pkg_role", - "warehouse": "pkg_wh", - "post_deploy": [ - {"sql_script": "scripts/script1.sql"}, - {"sql_script": "scripts/script2.sql"}, - ], - }, - "distribution": "external", - } - } - - -def app_v2(entity_id: str, from_pkg: str): - return { - entity_id: { - "type": "application", - "identifier": entity_id, - "from": {"target": from_pkg}, - "debug": True, - "meta": { - "role": "app_role", - "warehouse": "app_wh", - "post_deploy": [ - {"sql_script": "scripts/script3.sql"}, - {"sql_script": "scripts/script4.sql"}, - ], - }, - } - } - - -def native_app_v1(name: str, pkg: str, app: str): - napp = { - "name": name, - "artifacts": [{"src": "app/*", "dest": "./"}], - "source_stage": "app.stage", - "bundle_root": "bundle_root_path", - "generated_root": "generated_root_path", - "deploy_root": "deploy_root_path", - "scratch_stage": "scratch_stage_path", - "package": { - "name": pkg, - "distribution": "external", - "role": "pkg_role", - "warehouse": "pkg_wh", - "post_deploy": [ - {"sql_script": "scripts/script1.sql"}, - {"sql_script": "scripts/script2.sql"}, - ], - }, - } - if app: - napp["application"] = { - "name": app, - "role": "app_role", - "debug": True, - "warehouse": "app_wh", - "post_deploy": [ - {"sql_script": "scripts/script3.sql"}, - {"sql_script": "scripts/script4.sql"}, - ], - } - return napp - - -@pytest.mark.parametrize( - "pdfv2_input, expected_pdfv1, expected_error", - [ - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg"), - }, - }, - { - "definition_version": "1.1", - "native_app": native_app_v1("pkg", "pkg", ""), - }, - None, - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - }, - }, - None, - "More than one application package entity exists", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg"), - **app_v2("app", "pkg"), - }, - }, - { - "definition_version": "1.1", - "native_app": native_app_v1("app", "pkg", "app"), - }, - None, - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - **app_v2("app1", "pkg1"), - }, - }, - { - "definition_version": "1.1", - "native_app": native_app_v1("app1", "pkg1", "app1"), - }, - None, - ], - ], -) -def test_v2_to_v1_conversions(pdfv2_input, expected_pdfv1, expected_error): - pdfv2 = DefinitionV20(**pdfv2_input) - if expected_error: - with pytest.raises(ClickException, match=expected_error) as err: - _pdf_v2_to_v1(pdfv2) - else: - pdfv1_actual = vars(_pdf_v2_to_v1(pdfv2)) - pdfv1_expected = vars( - render_definition_template(expected_pdfv1, {}).project_definition - ) - - # Assert that the expected dict is a subset of the actual dict - assert {**pdfv1_actual, **pdfv1_expected} == pdfv1_actual - - -@pytest.mark.parametrize( - "pdfv2_input, target_pkg, target_app, app_required, expected_pdfv1, expected_error", - [ - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **app_v2("app1", "pkg1"), - **app_v2("app2", "pkg1"), - }, - }, - "", - "", - True, - None, - "More than one application entity exists in the project definition file, " - "specify --app-entity-id to choose which one to operate on.", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **app_v2("app1", "pkg1"), - **app_v2("app2", "pkg1"), - }, - }, - "", - "", - False, - { - "definition_version": "1.1", - "native_app": native_app_v1("pkg1", "pkg1", ""), - }, - None, - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - **app_v2("app2", "pkg1"), - }, - }, - "pkg2", - "app2", - True, - None, - "The application entity app2 does not " - "target the application package entity pkg2.", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - }, - }, - "", - "", - True, - None, - "Could not find an application entity in the project definition file.", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg"), - **app_v2("app1", "pkg"), - **app_v2("app2", "pkg"), - }, - }, - "", - "", - True, - None, - "More than one application entity exists in the project definition file, " - "specify --app-entity-id to choose which one to operate on.", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **app_v2("app1", "pkg1"), - **package_v2("pkg2"), - **app_v2("app2", "pkg2"), - }, - }, - "pkg2", - "app2", - True, - { - "definition_version": "1.1", - "native_app": native_app_v1("app2", "pkg2", "app2"), - }, - None, - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - }, - }, - "", - "", - False, - None, - "More than one application package entity exists in the project definition file, " - "specify --package-entity-id to choose which one to operate on.", - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **package_v2("pkg2"), - }, - }, - "pkg3", - "", - False, - None, - 'Could not find an application package entity with ID "pkg3" in the project definition file.', - ], - [ - { - "definition_version": "2", - "entities": { - **package_v2("pkg1"), - **app_v2("app1", "pkg1"), - **package_v2("pkg2"), - **app_v2("app2", "pkg2"), - }, - }, - "pkg2", - "app2", - False, - { - "definition_version": "1.1", - "native_app": native_app_v1("app2", "pkg2", "app2"), - }, - None, - ], - ], -) -def test_v2_to_v1_conversions_with_multiple_entities( - pdfv2_input, target_pkg, target_app, app_required, expected_pdfv1, expected_error -): - pdfv2 = DefinitionV20(**pdfv2_input) - if expected_error: - with pytest.raises(ClickException, match=expected_error) as err: - _pdf_v2_to_v1( - pdfv2, - package_entity_id=target_pkg, - app_entity_id=target_app, - app_required=app_required, - ) - else: - pdfv1_actual = vars( - _pdf_v2_to_v1( - pdfv2, - package_entity_id=target_pkg, - app_entity_id=target_app, - app_required=app_required, - ) - ) - pdfv1_expected = vars( - render_definition_template(expected_pdfv1, {}).project_definition - ) - - # Assert that the expected dict is a subset of the actual dict - assert {**pdfv1_actual, **pdfv1_expected} == pdfv1_actual - - -def test_decorator_error_when_no_project_exists(): - with pytest.raises(ValueError, match="Project definition could not be found"): - nativeapp_definition_v2_to_v1()(lambda *args: None)() - - -@pytest.mark.parametrize( - "pdfv2_input, expected_project_name", - [ - [ - # Using application name as project name - { - "definition_version": "2", - "entities": { - **package_v2("pkg"), - **app_v2("application_name", "pkg"), - }, - }, - "application_name", - ], - [ - # Using package name as project name - { - "definition_version": "2", - "entities": { - **package_v2("package_name"), - }, - }, - "package_name", - ], - [ - # Using package name as project name, stripping _pkg_.* - { - "definition_version": "2", - "entities": { - **package_v2("appname_pkg_username"), - }, - }, - "appname", - ], - ], -) -def test_project_name(pdfv2_input, expected_project_name): - pdfv2 = DefinitionV20(**pdfv2_input) - pdfv1 = _pdf_v2_to_v1(pdfv2) - - # Assert that the expected dict is a subset of the actual dict - assert pdfv1.native_app.name == expected_project_name - - -@mock.patch("snowflake.cli._plugins.nativeapp.v2_conversions.compat._pdf_v2_to_v1") -def test_decorator_skips_when_project_is_not_v2(mock_pdf_v2_to_v1): - pdfv1 = DefinitionV11( - **{ - "definition_version": "1.1", - "native_app": { - "name": "test", - "artifacts": [{"src": "*", "dest": "./"}], - }, - }, - ) - get_cli_context_manager().override_project_definition = pdfv1 - - nativeapp_definition_v2_to_v1()(lambda *args: None)() - - mock_pdf_v2_to_v1.launch.assert_not_called() - assert get_cli_context().project_definition == pdfv1