From 373f6f072b48f845ec4e22cff298701fc4ee654c Mon Sep 17 00:00:00 2001 From: Serina Grill <42048900+serinamarie@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:32:11 -0500 Subject: [PATCH] Update `prefect automation delete` (#12876) --- src/prefect/events/cli/automations.py | 56 +++++++++-- tests/events/client/cli/test_automations.py | 103 +++++++++++++++++++- 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/src/prefect/events/cli/automations.py b/src/prefect/events/cli/automations.py index dc316406c144..65af787ab70f 100644 --- a/src/prefect/events/cli/automations.py +++ b/src/prefect/events/cli/automations.py @@ -3,8 +3,10 @@ """ import functools +from typing import Optional import orjson +import typer import yaml as pyyaml from rich.pretty import Pretty from rich.table import Table @@ -149,15 +151,51 @@ async def pause(id_or_name: str): @automations_app.command() @requires_automations -async def delete(id_or_name: str): - """Delete an automation.""" - async with get_client() as client: - automation = await client.find_automation(id_or_name) +async def delete( + name: Optional[str] = typer.Argument(None, help="An automation's name"), + id: Optional[str] = typer.Option(None, "--id", help="An automation's id"), +): + """Delete an automation. - if not automation: - exit_with_success(f"Automation {id_or_name!r} not found.") + Arguments: + name: the name of the automation to delete + id: the id of the automation to delete - async with get_client() as client: - await client.delete_automation(automation.id) + Examples: + $ prefect automation delete "my-automation" + $ prefect automation delete --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + """ - exit_with_success(f"Deleted automation {automation.name!r} ({automation.id})") + async with get_client() as client: + if not id and not name: + exit_with_error("Please provide either a name or an id.") + + if id: + automation = await client.read_automation(id) + if not automation: + exit_with_error(f"Automation with id {id!r} not found.") + if not typer.confirm( + (f"Are you sure you want to delete automation with id {id!r}?"), + default=False, + ): + exit_with_error("Deletion aborted.") + await client.delete_automation(id) + exit_with_success(f"Deleted automation with id {id!r}") + + elif name: + automation = await client.read_automations_by_name(name=name) + if not automation: + exit_with_error( + f"Automation {name!r} not found. You can also specify an id with the `--id` flag." + ) + elif len(automation) > 1: + exit_with_error( + f"Multiple automations found with name {name!r}. Please specify an id with the `--id` flag instead." + ) + if not typer.confirm( + (f"Are you sure you want to delete automation with name {name!r}?"), + default=False, + ): + exit_with_error("Deletion aborted.") + await client.delete_automation(automation[0].id) + exit_with_success(f"Deleted automation with name {name!r}") diff --git a/tests/events/client/cli/test_automations.py b/tests/events/client/cli/test_automations.py index 04e7c6de4d43..b4b44723b492 100644 --- a/tests/events/client/cli/test_automations.py +++ b/tests/events/client/cli/test_automations.py @@ -85,6 +85,20 @@ def various_automations(read_automations: mock.AsyncMock) -> List[Automation]: actions_on_trigger=[DoNothing()], actions_on_resolve=[PauseAutomation(automation_id=uuid4())], ), + Automation( + id=UUID("dddddddd-dddd-dddd-dddd-dddddddddddd"), + name="A Metric one", + trigger=MetricTrigger( + metric=MetricTriggerQuery( + name=PrefectMetric.successes, + operator=MetricTriggerOperator.LT, + threshold=0.78, + ) + ), + actions=[CancelFlowRun()], + actions_on_trigger=[DoNothing()], + actions_on_resolve=[PauseAutomation(automation_id=uuid4())], + ), ] read_automations.return_value = automations return automations @@ -273,13 +287,31 @@ def delete_automation() -> Generator[mock.AsyncMock, None, None]: yield m +@pytest.fixture +def read_automations_by_name() -> Generator[mock.AsyncMock, None, None]: + with mock.patch( + "prefect.client.orchestration.PrefectClient.read_automations_by_name", + autospec=True, + ) as mock_read: + yield mock_read + + def test_deleting_by_name( - delete_automation: mock.AsyncMock, various_automations: List[Automation] + delete_automation: mock.AsyncMock, + read_automations_by_name: mock.AsyncMock, + various_automations: List[Automation], ): + read_automations_by_name.return_value = [various_automations[0]] invoke_and_assert( ["automations", "delete", "My First Reactive"], + prompts_and_responses=[ + ( + "Are you sure you want to delete automation with name 'My First Reactive'?", + "y", + ) + ], expected_code=0, - expected_output_contains=["Deleted automation 'My First Reactive'"], + expected_output_contains=["Deleted automation with name 'My First Reactive'"], ) delete_automation.assert_awaited_once_with( @@ -287,13 +319,74 @@ def test_deleting_by_name( ) -def test_deleting_not_found_is_a_noop( - delete_automation: mock.AsyncMock, various_automations: List[Automation] +def test_deleting_by_name_multiple_same_name( + delete_automation: mock.AsyncMock, + read_automations_by_name: mock.AsyncMock, + various_automations: List[Automation], +): + read_automations_by_name.return_value = various_automations[:2] + invoke_and_assert( + ["automations", "delete", "A Metric one"], + expected_code=1, + expected_output_contains=[ + "Multiple automations found with name 'A Metric one'. Please specify an id with the `--id` flag instead." + ], + ) + + delete_automation.assert_not_called() + + +def test_deleting_by_id_not_found_is_a_noop( + delete_automation: mock.AsyncMock, + various_automations: List[Automation], + read_automations_by_name: mock.AsyncMock, ): + read_automations_by_name.return_value = None invoke_and_assert( ["automations", "delete", "Who dis?"], - expected_code=0, + expected_code=1, expected_output_contains=["Automation 'Who dis?' not found"], ) delete_automation.assert_not_called() + + +def test_deleting_by_id( + delete_automation: mock.AsyncMock, + read_automation: mock.AsyncMock, + various_automations: List[Automation], +): + read_automation.return_value = various_automations[0] + invoke_and_assert( + ["automations", "delete", "--id", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"], + prompts_and_responses=[ + ( + "Are you sure you want to delete automation with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'?", + "y", + ) + ], + expected_code=0, + expected_output_contains=[ + "Deleted automation with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'" + ], + ) + + delete_automation.assert_awaited_once_with( + mock.ANY, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + ) + + +def test_deleting_by_nonexistent_id( + delete_automation: mock.AsyncMock, + read_automation: mock.AsyncMock, +): + read_automation.return_value = None + invoke_and_assert( + ["automations", "delete", "--id", "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"], + expected_code=1, + expected_output_contains=[ + "Automation with id 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz' not found" + ], + ) + + delete_automation.assert_not_called()