Skip to content

Commit d251fb5

Browse files
authored
Update automation pause and automation resume to handle automations with same name (#13131)
1 parent 73fd9ee commit d251fb5

File tree

2 files changed

+235
-31
lines changed

2 files changed

+235
-31
lines changed

src/prefect/events/cli/automations.py

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from prefect.cli._utilities import exit_with_error, exit_with_success
1818
from prefect.cli.root import app
1919
from prefect.client.orchestration import get_client
20+
from prefect.events.schemas.automations import Automation
2021
from prefect.exceptions import PrefectHTTPStatusError
2122

2223
automations_app = PrefectTyper(
@@ -149,7 +150,7 @@ async def inspect(
149150
if yaml or json:
150151
if isinstance(automation, list):
151152
automation = [a.dict(json_compatible=True) for a in automation]
152-
elif isinstance(automation, dict):
153+
elif isinstance(automation, Automation):
153154
automation = automation.dict(json_compatible=True)
154155
if yaml:
155156
app.console.print(pyyaml.dump(automation, sort_keys=False))
@@ -163,32 +164,112 @@ async def inspect(
163164

164165
@automations_app.command(aliases=["enable"])
165166
@requires_automations
166-
async def resume(id_or_name: str):
167-
"""Resume an automation."""
168-
async with get_client() as client:
169-
automation = await client.find_automation(id_or_name)
170-
if not automation:
171-
exit_with_error(f"Automation {id_or_name!r} not found.")
167+
async def resume(
168+
name: Optional[str] = typer.Argument(None, help="An automation's name"),
169+
id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
170+
):
171+
"""
172+
Resume an automation.
172173
173-
async with get_client() as client:
174-
await client.resume_automation(automation.id)
174+
Arguments:
175+
176+
name: the name of the automation to resume
177+
178+
id: the id of the automation to resume
179+
180+
Examples:
181+
182+
$ prefect automation resume "my-automation"
183+
184+
$ prefect automation resume --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
185+
"""
186+
if not id and not name:
187+
exit_with_error("Please provide either a name or an id.")
175188

176-
exit_with_success(f"Resumed automation {automation.name!r} ({automation.id})")
189+
if name:
190+
async with get_client() as client:
191+
automation = await client.read_automations_by_name(name=name)
192+
if not automation:
193+
exit_with_error(
194+
f"Automation with name {name!r} not found. You can also specify an id with the `--id` flag."
195+
)
196+
if len(automation) > 1:
197+
if not typer.confirm(
198+
f"Multiple automations found with name {name!r}. Do you want to resume all of them?",
199+
default=False,
200+
):
201+
exit_with_error("Resume aborted.")
202+
203+
for a in automation:
204+
await client.resume_automation(a.id)
205+
exit_with_success(
206+
f"Resumed automation(s) with name {name!r} and id(s) {', '.join([repr(str(a.id)) for a in automation])}."
207+
)
208+
209+
elif id:
210+
async with get_client() as client:
211+
try:
212+
uuid_id = UUID(id)
213+
automation = await client.read_automation(uuid_id)
214+
except (PrefectHTTPStatusError, ValueError):
215+
exit_with_error(f"Automation with id {id!r} not found.")
216+
await client.resume_automation(automation.id)
217+
exit_with_success(f"Resumed automation with id {str(automation.id)!r}.")
177218

178219

179220
@automations_app.command(aliases=["disable"])
180221
@requires_automations
181-
async def pause(id_or_name: str):
182-
"""Pause an automation."""
183-
async with get_client() as client:
184-
automation = await client.find_automation(id_or_name)
185-
if not automation:
186-
exit_with_error(f"Automation {id_or_name!r} not found.")
222+
async def pause(
223+
name: Optional[str] = typer.Argument(None, help="An automation's name"),
224+
id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
225+
):
226+
"""
227+
Pause an automation.
187228
188-
async with get_client() as client:
189-
await client.pause_automation(automation.id)
229+
Arguments:
230+
231+
name: the name of the automation to pause
232+
233+
id: the id of the automation to pause
234+
235+
Examples:
236+
237+
$ prefect automation pause "my-automation"
238+
239+
$ prefect automation pause --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
240+
"""
241+
if not id and not name:
242+
exit_with_error("Please provide either a name or an id.")
190243

191-
exit_with_success(f"Paused automation {automation.name!r} ({automation.id})")
244+
if name:
245+
async with get_client() as client:
246+
automation = await client.read_automations_by_name(name=name)
247+
if not automation:
248+
exit_with_error(
249+
f"Automation with name {name!r} not found. You can also specify an id with the `--id` flag."
250+
)
251+
if len(automation) > 1:
252+
if not typer.confirm(
253+
f"Multiple automations found with name {name!r}. Do you want to pause all of them?",
254+
default=False,
255+
):
256+
exit_with_error("Pause aborted.")
257+
258+
for a in automation:
259+
await client.pause_automation(a.id)
260+
exit_with_success(
261+
f"Paused automation(s) with name {name!r} and id(s) {', '.join([repr(str(a.id)) for a in automation])}."
262+
)
263+
264+
elif id:
265+
async with get_client() as client:
266+
try:
267+
uuid_id = UUID(id)
268+
automation = await client.read_automation(uuid_id)
269+
except (PrefectHTTPStatusError, ValueError):
270+
exit_with_error(f"Automation with id {id!r} not found.")
271+
await client.pause_automation(automation.id)
272+
exit_with_success(f"Paused automation with id {str(automation.id)!r}.")
192273

193274

194275
@automations_app.command()

tests/events/client/cli/test_automations.py

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def test_inspecting_by_name_not_found(
211211
)
212212

213213

214-
def test_inspecting_in_json(
214+
def test_inspecting_by_name_in_json(
215215
various_automations: List[Automation], read_automations_by_name: mock.AsyncMock
216216
):
217217
read_automations_by_name.return_value = [various_automations[0]]
@@ -223,7 +223,26 @@ def test_inspecting_in_json(
223223
assert loaded[0]["id"] == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
224224

225225

226-
def test_inspecting_in_yaml(
226+
def test_inspecting_by_id_in_json(
227+
various_automations: List[Automation], read_automation: mock.AsyncMock
228+
):
229+
read_automation.return_value = various_automations[1]
230+
result = invoke_and_assert(
231+
[
232+
"automations",
233+
"inspect",
234+
"--id",
235+
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
236+
"--json",
237+
],
238+
expected_code=0,
239+
)
240+
loaded = orjson.loads(result.output)
241+
assert loaded["name"] == "My Other Reactive Automation"
242+
assert loaded["id"] == "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
243+
244+
245+
def test_inspecting_by_name_in_yaml(
227246
various_automations: List[Automation], read_automations_by_name: mock.AsyncMock
228247
):
229248
read_automations_by_name.return_value = [various_automations[0]]
@@ -235,6 +254,25 @@ def test_inspecting_in_yaml(
235254
assert loaded[0]["id"] == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
236255

237256

257+
def test_inspecting_by_id_in_yaml(
258+
various_automations: List[Automation], read_automation: mock.AsyncMock
259+
):
260+
read_automation.return_value = various_automations[1]
261+
result = invoke_and_assert(
262+
[
263+
"automations",
264+
"inspect",
265+
"--id",
266+
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
267+
"--yaml",
268+
],
269+
expected_code=0,
270+
)
271+
loaded = yaml.safe_load(result.output)
272+
assert loaded["name"] == "My Other Reactive Automation"
273+
assert loaded["id"] == "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
274+
275+
238276
@pytest.fixture
239277
def pause_automation() -> Generator[mock.AsyncMock, None, None]:
240278
with mock.patch(
@@ -244,26 +282,69 @@ def pause_automation() -> Generator[mock.AsyncMock, None, None]:
244282

245283

246284
def test_pausing_by_name(
247-
pause_automation: mock.AsyncMock, various_automations: List[Automation]
285+
pause_automation: mock.AsyncMock,
286+
various_automations: List[Automation],
287+
read_automations_by_name: mock.AsyncMock,
248288
):
289+
read_automations_by_name.return_value = [various_automations[0]]
249290
invoke_and_assert(
250291
["automations", "pause", "My First Reactive"],
251292
expected_code=0,
252-
expected_output_contains=["Paused automation 'My First Reactive'"],
293+
expected_output_contains=[
294+
"Paused automation(s) with name 'My First Reactive' and id(s) 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'"
295+
],
253296
)
254297

255298
pause_automation.assert_awaited_once_with(
256299
mock.ANY, UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
257300
)
258301

259302

260-
def test_pausing_not_found(
261-
pause_automation: mock.AsyncMock, various_automations: List[Automation]
303+
def test_pausing_by_name_not_found(
304+
pause_automation: mock.AsyncMock,
305+
various_automations: List[Automation],
306+
read_automations_by_name: mock.AsyncMock,
262307
):
308+
read_automations_by_name.return_value = None
263309
invoke_and_assert(
264310
["automations", "pause", "Wha?"],
265311
expected_code=1,
266-
expected_output_contains=["Automation 'Wha?' not found"],
312+
expected_output_contains=["Automation with name 'Wha?' not found"],
313+
)
314+
315+
pause_automation.assert_not_awaited()
316+
317+
318+
def test_pausing_by_id(
319+
pause_automation: mock.AsyncMock,
320+
various_automations: List[Automation],
321+
read_automation: mock.AsyncMock,
322+
):
323+
read_automation.return_value = various_automations[0]
324+
invoke_and_assert(
325+
["automations", "pause", "--id", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"],
326+
expected_code=0,
327+
expected_output_contains=[
328+
"Paused automation with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'"
329+
],
330+
)
331+
332+
pause_automation.assert_awaited_once_with(
333+
mock.ANY, UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
334+
)
335+
336+
337+
def test_pausing_by_id_not_found(
338+
pause_automation: mock.AsyncMock,
339+
read_automation: mock.AsyncMock,
340+
):
341+
read_automation.return_value = None
342+
invoke_and_assert(
343+
["automations", "pause", "--id", "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"],
344+
expected_code=1,
345+
expected_output_contains=[
346+
"Automation with id 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz' not found"
347+
],
267348
)
268349

269350
pause_automation.assert_not_awaited()
@@ -278,26 +359,68 @@ def resume_automation() -> Generator[mock.AsyncMock, None, None]:
278359

279360

280361
def test_resuming_by_name(
281-
resume_automation: mock.AsyncMock, various_automations: List[Automation]
362+
resume_automation: mock.AsyncMock,
363+
various_automations: List[Automation],
364+
read_automations_by_name: mock.AsyncMock,
282365
):
366+
read_automations_by_name.return_value = [various_automations[0]]
283367
invoke_and_assert(
284368
["automations", "resume", "My First Reactive"],
285369
expected_code=0,
286-
expected_output_contains=["Resumed automation 'My First Reactive'"],
370+
expected_output_contains=[
371+
"Resumed automation(s) with name 'My First Reactive' and id(s) 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'"
372+
],
287373
)
288374

289375
resume_automation.assert_awaited_once_with(
290376
mock.ANY, UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
291377
)
292378

293379

294-
def test_resuming_not_found(
295-
resume_automation: mock.AsyncMock, various_automations: List[Automation]
380+
def test_resuming_by_name_not_found(
381+
resume_automation: mock.AsyncMock,
382+
various_automations: List[Automation],
383+
read_automations_by_name: mock.AsyncMock,
296384
):
385+
read_automations_by_name.return_value = None
297386
invoke_and_assert(
298387
["automations", "resume", "Wha?"],
299388
expected_code=1,
300-
expected_output_contains=["Automation 'Wha?' not found"],
389+
expected_output_contains=["Automation with name 'Wha?' not found"],
390+
)
391+
392+
resume_automation.assert_not_awaited()
393+
394+
395+
def test_resuming_by_id(
396+
resume_automation: mock.AsyncMock,
397+
various_automations: List[Automation],
398+
read_automation: mock.AsyncMock,
399+
):
400+
read_automation.return_value = various_automations[0]
401+
invoke_and_assert(
402+
["automations", "resume", "--id", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"],
403+
expected_code=0,
404+
expected_output_contains=[
405+
"Resumed automation with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'"
406+
],
407+
)
408+
409+
resume_automation.assert_awaited_once_with(
410+
mock.ANY, UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
411+
)
412+
413+
414+
def test_resuming_by_id_not_found(
415+
resume_automation: mock.AsyncMock, read_automation: mock.AsyncMock
416+
):
417+
read_automation.return_value = None
418+
invoke_and_assert(
419+
["automations", "resume", "--id", "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"],
420+
expected_code=1,
421+
expected_output_contains=[
422+
"Automation with id 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz' not found"
423+
],
301424
)
302425

303426
resume_automation.assert_not_awaited()

0 commit comments

Comments
 (0)