prefect-client 2.17.1__py3-none-any.whl → 2.18.1__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 (71) hide show
  1. prefect/_internal/compatibility/deprecated.py +2 -0
  2. prefect/_internal/pydantic/_compat.py +1 -0
  3. prefect/_internal/pydantic/utilities/field_validator.py +25 -10
  4. prefect/_internal/pydantic/utilities/model_dump.py +1 -1
  5. prefect/_internal/pydantic/utilities/model_validate.py +1 -1
  6. prefect/_internal/pydantic/utilities/model_validator.py +11 -3
  7. prefect/_internal/schemas/fields.py +31 -12
  8. prefect/_internal/schemas/validators.py +0 -6
  9. prefect/_version.py +97 -38
  10. prefect/blocks/abstract.py +34 -1
  11. prefect/blocks/core.py +1 -1
  12. prefect/blocks/notifications.py +16 -7
  13. prefect/blocks/system.py +2 -3
  14. prefect/client/base.py +10 -5
  15. prefect/client/orchestration.py +405 -85
  16. prefect/client/schemas/actions.py +4 -3
  17. prefect/client/schemas/objects.py +6 -5
  18. prefect/client/schemas/schedules.py +2 -6
  19. prefect/client/schemas/sorting.py +9 -0
  20. prefect/client/utilities.py +25 -3
  21. prefect/concurrency/asyncio.py +11 -5
  22. prefect/concurrency/events.py +3 -3
  23. prefect/concurrency/services.py +1 -1
  24. prefect/concurrency/sync.py +9 -5
  25. prefect/deployments/__init__.py +0 -2
  26. prefect/deployments/base.py +2 -144
  27. prefect/deployments/deployments.py +29 -20
  28. prefect/deployments/runner.py +36 -28
  29. prefect/deployments/steps/core.py +3 -3
  30. prefect/deprecated/packaging/serializers.py +5 -4
  31. prefect/engine.py +3 -1
  32. prefect/events/__init__.py +45 -0
  33. prefect/events/actions.py +250 -18
  34. prefect/events/cli/automations.py +201 -0
  35. prefect/events/clients.py +179 -21
  36. prefect/events/filters.py +30 -3
  37. prefect/events/instrument.py +40 -40
  38. prefect/events/related.py +2 -1
  39. prefect/events/schemas/automations.py +126 -8
  40. prefect/events/schemas/deployment_triggers.py +23 -277
  41. prefect/events/schemas/events.py +7 -7
  42. prefect/events/utilities.py +3 -1
  43. prefect/events/worker.py +21 -8
  44. prefect/exceptions.py +1 -1
  45. prefect/flows.py +33 -18
  46. prefect/input/actions.py +9 -9
  47. prefect/input/run_input.py +49 -37
  48. prefect/logging/__init__.py +2 -2
  49. prefect/logging/loggers.py +64 -1
  50. prefect/new_flow_engine.py +293 -0
  51. prefect/new_task_engine.py +374 -0
  52. prefect/results.py +32 -12
  53. prefect/runner/runner.py +3 -2
  54. prefect/serializers.py +62 -31
  55. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +44 -3
  56. prefect/settings.py +32 -10
  57. prefect/states.py +25 -19
  58. prefect/tasks.py +17 -0
  59. prefect/types/__init__.py +90 -0
  60. prefect/utilities/asyncutils.py +37 -0
  61. prefect/utilities/engine.py +6 -4
  62. prefect/utilities/pydantic.py +34 -15
  63. prefect/utilities/schema_tools/hydration.py +88 -19
  64. prefect/utilities/schema_tools/validation.py +1 -1
  65. prefect/variables.py +4 -4
  66. {prefect_client-2.17.1.dist-info → prefect_client-2.18.1.dist-info}/METADATA +1 -1
  67. {prefect_client-2.17.1.dist-info → prefect_client-2.18.1.dist-info}/RECORD +71 -67
  68. /prefect/{concurrency/common.py → events/cli/__init__.py} +0 -0
  69. {prefect_client-2.17.1.dist-info → prefect_client-2.18.1.dist-info}/LICENSE +0 -0
  70. {prefect_client-2.17.1.dist-info → prefect_client-2.18.1.dist-info}/WHEEL +0 -0
  71. {prefect_client-2.17.1.dist-info → prefect_client-2.18.1.dist-info}/top_level.txt +0 -0
prefect/engine.py CHANGED
@@ -1410,7 +1410,9 @@ def enter_task_run_engine(
1410
1410
  task_runner=task_runner,
1411
1411
  )
1412
1412
 
1413
- if task.isasync and flow_run_context.flow.isasync:
1413
+ if task.isasync and (
1414
+ flow_run_context.flow is None or flow_run_context.flow.isasync
1415
+ ):
1414
1416
  # return a coro for the user to await if an async task in an async flow
1415
1417
  return from_async.wait_for_call_in_loop_thread(begin_run)
1416
1418
  else:
@@ -2,7 +2,9 @@ from .schemas.events import Event, ReceivedEvent
2
2
  from .schemas.events import Resource, RelatedResource, ResourceSpecification
3
3
  from .schemas.automations import (
4
4
  Automation,
5
+ AutomationCore,
5
6
  Posture,
7
+ TriggerTypes,
6
8
  Trigger,
7
9
  ResourceTrigger,
8
10
  EventTrigger,
@@ -20,6 +22,27 @@ from .schemas.deployment_triggers import (
20
22
  DeploymentCompoundTrigger,
21
23
  DeploymentSequenceTrigger,
22
24
  )
25
+ from .actions import (
26
+ ActionTypes,
27
+ Action,
28
+ DoNothing,
29
+ RunDeployment,
30
+ PauseDeployment,
31
+ ResumeDeployment,
32
+ ChangeFlowRunState,
33
+ CancelFlowRun,
34
+ SuspendFlowRun,
35
+ CallWebhook,
36
+ SendNotification,
37
+ PauseWorkPool,
38
+ ResumeWorkPool,
39
+ PauseWorkQueue,
40
+ ResumeWorkQueue,
41
+ PauseAutomation,
42
+ ResumeAutomation,
43
+ DeclareIncident,
44
+ )
45
+ from .clients import get_events_client, get_events_subscriber
23
46
  from .utilities import emit_event
24
47
 
25
48
  __all__ = [
@@ -29,7 +52,9 @@ __all__ = [
29
52
  "RelatedResource",
30
53
  "ResourceSpecification",
31
54
  "Automation",
55
+ "AutomationCore",
32
56
  "Posture",
57
+ "TriggerTypes",
33
58
  "Trigger",
34
59
  "ResourceTrigger",
35
60
  "EventTrigger",
@@ -44,5 +69,25 @@ __all__ = [
44
69
  "DeploymentMetricTrigger",
45
70
  "DeploymentCompoundTrigger",
46
71
  "DeploymentSequenceTrigger",
72
+ "ActionTypes",
73
+ "Action",
74
+ "DoNothing",
75
+ "RunDeployment",
76
+ "PauseDeployment",
77
+ "ResumeDeployment",
78
+ "ChangeFlowRunState",
79
+ "CancelFlowRun",
80
+ "SuspendFlowRun",
81
+ "CallWebhook",
82
+ "SendNotification",
83
+ "PauseWorkPool",
84
+ "ResumeWorkPool",
85
+ "PauseWorkQueue",
86
+ "ResumeWorkQueue",
87
+ "PauseAutomation",
88
+ "ResumeAutomation",
89
+ "DeclareIncident",
47
90
  "emit_event",
91
+ "get_events_client",
92
+ "get_events_subscriber",
48
93
  ]
prefect/events/actions.py CHANGED
@@ -1,36 +1,71 @@
1
+ import abc
1
2
  from typing import Any, Dict, Optional, Union
2
3
  from uuid import UUID
3
4
 
5
+ from typing_extensions import Literal, TypeAlias
6
+
4
7
  from prefect._internal.pydantic import HAS_PYDANTIC_V2
5
8
 
6
9
  if HAS_PYDANTIC_V2:
7
- from pydantic.v1 import Field
10
+ from pydantic.v1 import Field, root_validator
8
11
  else:
9
- from pydantic import Field
10
-
11
- from typing_extensions import Literal
12
+ from pydantic import Field, root_validator # type: ignore
12
13
 
13
14
  from prefect._internal.schemas.bases import PrefectBaseModel
15
+ from prefect.client.schemas.objects import StateType
14
16
 
15
17
 
16
- class Action(PrefectBaseModel):
18
+ class Action(PrefectBaseModel, abc.ABC):
17
19
  """An Action that may be performed when an Automation is triggered"""
18
20
 
19
21
  type: str
20
22
 
23
+ def describe_for_cli(self) -> str:
24
+ """A human-readable description of the action"""
25
+ return self.type.replace("-", " ").capitalize()
26
+
21
27
 
22
28
  class DoNothing(Action):
23
- """Do nothing, which may be helpful for testing automations"""
29
+ """Do nothing when an Automation is triggered"""
24
30
 
25
31
  type: Literal["do-nothing"] = "do-nothing"
26
32
 
27
33
 
28
- class RunDeployment(Action):
29
- """Run the given deployment with the given parameters"""
34
+ class DeploymentAction(Action):
35
+ """Base class for Actions that operate on Deployments and need to infer them from
36
+ events"""
37
+
38
+ source: Literal["selected", "inferred"] = Field(
39
+ "selected",
40
+ description=(
41
+ "Whether this Action applies to a specific selected "
42
+ "deployment (given by `deployment_id`), or to a deployment that is "
43
+ "inferred from the triggering event. If the source is 'inferred', "
44
+ "the `deployment_id` may not be set. If the source is 'selected', the "
45
+ "`deployment_id` must be set."
46
+ ),
47
+ )
48
+ deployment_id: Optional[UUID] = Field(
49
+ None, description="The identifier of the deployment"
50
+ )
51
+
52
+ @root_validator
53
+ def selected_deployment_requires_id(cls, values):
54
+ wants_selected_deployment = values.get("source") == "selected"
55
+ has_deployment_id = bool(values.get("deployment_id"))
56
+ if wants_selected_deployment != has_deployment_id:
57
+ raise ValueError(
58
+ "deployment_id is "
59
+ + ("not allowed" if has_deployment_id else "required")
60
+ )
61
+ return values
62
+
63
+
64
+ class RunDeployment(DeploymentAction):
65
+ """Runs the given deployment with the given parameters"""
30
66
 
31
67
  type: Literal["run-deployment"] = "run-deployment"
32
68
 
33
- source: Literal["selected"] = "selected"
34
69
  parameters: Optional[Dict[str, Any]] = Field(
35
70
  None,
36
71
  description=(
@@ -38,26 +73,223 @@ class RunDeployment(Action):
38
73
  "deployment's default parameters"
39
74
  ),
40
75
  )
41
- deployment_id: UUID = Field(..., description="The identifier of the deployment")
42
76
  job_variables: Optional[Dict[str, Any]] = Field(
43
77
  None,
44
78
  description=(
45
- "Job variables to pass to the run, or None to use the "
46
- "deployment's default job variables"
79
+ "The job variables to pass to the created flow run, or None "
80
+ "to use the deployment's default job variables"
47
81
  ),
48
82
  )
49
83
 
50
84
 
85
+ class PauseDeployment(DeploymentAction):
86
+ """Pauses the given Deployment"""
87
+
88
+ type: Literal["pause-deployment"] = "pause-deployment"
89
+
90
+
91
+ class ResumeDeployment(DeploymentAction):
92
+ """Resumes the given Deployment"""
93
+
94
+ type: Literal["resume-deployment"] = "resume-deployment"
95
+
96
+
97
+ class ChangeFlowRunState(Action):
98
+ """Changes the state of a flow run associated with the trigger"""
99
+
100
+ type: Literal["change-flow-run-state"] = "change-flow-run-state"
101
+
102
+ name: Optional[str] = Field(
103
+ None,
104
+ description="The name of the state to change the flow run to",
105
+ )
106
+ state: StateType = Field(
107
+ ...,
108
+ description="The type of the state to change the flow run to",
109
+ )
110
+ message: Optional[str] = Field(
111
+ None,
112
+ description="An optional message to associate with the state change",
113
+ )
114
+
115
+
116
+ class CancelFlowRun(Action):
117
+ """Cancels a flow run associated with the trigger"""
118
+
119
+ type: Literal["cancel-flow-run"] = "cancel-flow-run"
120
+
121
+
122
+ class SuspendFlowRun(Action):
123
+ """Suspends a flow run associated with the trigger"""
124
+
125
+ type: Literal["suspend-flow-run"] = "suspend-flow-run"
126
+
127
+
128
+ class CallWebhook(Action):
129
+ """Call a webhook when an Automation is triggered."""
130
+
131
+ type: Literal["call-webhook"] = "call-webhook"
132
+ block_document_id: UUID = Field(
133
+ description="The identifier of the webhook block to use"
134
+ )
135
+ payload: str = Field(
136
+ default="",
137
+ description="An optional templatable payload to send when calling the webhook.",
138
+ )
139
+
140
+
51
141
  class SendNotification(Action):
52
- """Send a notification with the given parameters"""
142
+ """Send a notification when an Automation is triggered"""
53
143
 
54
144
  type: Literal["send-notification"] = "send-notification"
55
-
56
145
  block_document_id: UUID = Field(
57
- ..., description="The identifier of the notification block"
146
+ description="The identifier of the notification block to use"
147
+ )
148
+ subject: str = Field("Prefect automated notification")
149
+ body: str = Field(description="The text of the notification to send")
150
+
151
+
152
+ class WorkPoolAction(Action):
153
+ """Base class for Actions that operate on Work Pools and need to infer them from
154
+ events"""
155
+
156
+ source: Literal["selected", "inferred"] = Field(
157
+ "selected",
158
+ description=(
159
+ "Whether this Action applies to a specific selected "
160
+ "work pool (given by `work_pool_id`), or to a work pool that is "
161
+ "inferred from the triggering event. If the source is 'inferred', "
162
+ "the `work_pool_id` may not be set. If the source is 'selected', the "
163
+ "`work_pool_id` must be set."
164
+ ),
165
+ )
166
+ work_pool_id: Optional[UUID] = Field(
167
+ None,
168
+ description="The identifier of the work pool to pause",
169
+ )
170
+
171
+
172
+ class PauseWorkPool(WorkPoolAction):
173
+ """Pauses a Work Pool"""
174
+
175
+ type: Literal["pause-work-pool"] = "pause-work-pool"
176
+
177
+
178
+ class ResumeWorkPool(WorkPoolAction):
179
+ """Resumes a Work Pool"""
180
+
181
+ type: Literal["resume-work-pool"] = "resume-work-pool"
182
+
183
+
184
+ class WorkQueueAction(Action):
185
+ """Base class for Actions that operate on Work Queues and need to infer them from
186
+ events"""
187
+
188
+ source: Literal["selected", "inferred"] = Field(
189
+ "selected",
190
+ description=(
191
+ "Whether this Action applies to a specific selected "
192
+ "work queue (given by `work_queue_id`), or to a work queue that is "
193
+ "inferred from the triggering event. If the source is 'inferred', "
194
+ "the `work_queue_id` may not be set. If the source is 'selected', the "
195
+ "`work_queue_id` must be set."
196
+ ),
197
+ )
198
+ work_queue_id: Optional[UUID] = Field(
199
+ None, description="The identifier of the work queue to pause"
200
+ )
201
+
202
+ @root_validator
203
+ def selected_work_queue_requires_id(cls, values):
204
+ wants_selected_work_queue = values.get("source") == "selected"
205
+ has_work_queue_id = bool(values.get("work_queue_id"))
206
+ if wants_selected_work_queue != has_work_queue_id:
207
+ raise ValueError(
208
+ "work_queue_id is "
209
+ + ("not allowed" if has_work_queue_id else "required")
210
+ )
211
+ return values
212
+
213
+
214
+ class PauseWorkQueue(WorkQueueAction):
215
+ """Pauses a Work Queue"""
216
+
217
+ type: Literal["pause-work-queue"] = "pause-work-queue"
218
+
219
+
220
+ class ResumeWorkQueue(WorkQueueAction):
221
+ """Resumes a Work Queue"""
222
+
223
+ type: Literal["resume-work-queue"] = "resume-work-queue"
224
+
225
+
226
+ class AutomationAction(Action):
227
+ """Base class for Actions that operate on Automations and need to infer them from
228
+ events"""
229
+
230
+ source: Literal["selected", "inferred"] = Field(
231
+ "selected",
232
+ description=(
233
+ "Whether this Action applies to a specific selected "
234
+ "automation (given by `automation_id`), or to an automation that is "
235
+ "inferred from the triggering event. If the source is 'inferred', "
236
+ "the `automation_id` may not be set. If the source is 'selected', the "
237
+ "`automation_id` must be set."
238
+ ),
239
+ )
240
+ automation_id: Optional[UUID] = Field(
241
+ None, description="The identifier of the automation to act on"
58
242
  )
59
- body: str = Field(..., description="Notification body")
60
- subject: Optional[str] = Field(None, description="Notification subject")
243
+
244
+ @root_validator
245
+ def selected_automation_requires_id(cls, values):
246
+ wants_selected_automation = values.get("source") == "selected"
247
+ has_automation_id = bool(values.get("automation_id"))
248
+ if wants_selected_automation != has_automation_id:
249
+ raise ValueError(
250
+ "automation_id is "
251
+ + ("not allowed" if has_automation_id else "required")
252
+ )
253
+ return values
254
+
255
+
256
+ class PauseAutomation(AutomationAction):
257
+ """Pauses a Work Queue"""
258
+
259
+ type: Literal["pause-automation"] = "pause-automation"
260
+
261
+
262
+ class ResumeAutomation(AutomationAction):
263
+ """Resumes a Work Queue"""
264
+
265
+ type: Literal["resume-automation"] = "resume-automation"
266
+
267
+
268
+ class DeclareIncident(Action):
269
+ """Declares an incident for the triggering event. Only available on Prefect Cloud"""
270
+
271
+ type: Literal["declare-incident"] = "declare-incident"
61
272
 
62
273
 
63
- ActionTypes = Union[DoNothing, RunDeployment, SendNotification]
274
+ # The actual action types that we support. It's important to update this
275
+ # Union when adding new subclasses of Action so that they are available for clients
276
+ # and in the OpenAPI docs
277
+ ActionTypes: TypeAlias = Union[
278
+ DoNothing,
279
+ RunDeployment,
280
+ PauseDeployment,
281
+ ResumeDeployment,
282
+ CancelFlowRun,
283
+ ChangeFlowRunState,
284
+ PauseWorkQueue,
285
+ ResumeWorkQueue,
286
+ SendNotification,
287
+ CallWebhook,
288
+ PauseAutomation,
289
+ ResumeAutomation,
290
+ SuspendFlowRun,
291
+ PauseWorkPool,
292
+ ResumeWorkPool,
293
+ # Prefect Cloud only
294
+ DeclareIncident,
295
+ ]
@@ -0,0 +1,201 @@
1
+ """
2
+ Command line interface for working with automations.
3
+ """
4
+
5
+ import functools
6
+ from typing import Optional
7
+
8
+ import orjson
9
+ import typer
10
+ import yaml as pyyaml
11
+ from rich.pretty import Pretty
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from prefect.cli._types import PrefectTyper
16
+ from prefect.cli._utilities import exit_with_error, exit_with_success
17
+ from prefect.cli.root import app
18
+ from prefect.client.orchestration import get_client
19
+
20
+ automations_app = PrefectTyper(
21
+ name="automation",
22
+ help="Commands for managing automations.",
23
+ )
24
+ app.add_typer(automations_app, aliases=["automations"])
25
+
26
+
27
+ def requires_automations(func):
28
+ @functools.wraps(func)
29
+ async def wrapper(*args, **kwargs):
30
+ try:
31
+ return await func(*args, **kwargs)
32
+ except RuntimeError as exc:
33
+ if "Enable experimental" in str(exc):
34
+ exit_with_error(str(exc))
35
+ raise
36
+
37
+ return wrapper
38
+
39
+
40
+ @automations_app.command()
41
+ @requires_automations
42
+ async def ls():
43
+ """List all automations."""
44
+ async with get_client() as client:
45
+ automations = await client.read_automations()
46
+
47
+ table = Table(title="Automations", show_lines=True)
48
+
49
+ table.add_column("Automation")
50
+ table.add_column("Enabled")
51
+ table.add_column("Trigger")
52
+ table.add_column("Actions")
53
+
54
+ for automation in automations:
55
+ identifier_column = Text()
56
+
57
+ identifier_column.append(automation.name, style="white bold")
58
+ identifier_column.append("\n")
59
+
60
+ identifier_column.append(str(automation.id), style="cyan")
61
+ identifier_column.append("\n")
62
+
63
+ if automation.description:
64
+ identifier_column.append(automation.description, style="white")
65
+ identifier_column.append("\n")
66
+
67
+ if automation.actions_on_trigger or automation.actions_on_resolve:
68
+ actions = (
69
+ [
70
+ f"(trigger) {action.describe_for_cli()}"
71
+ for action in automation.actions
72
+ ]
73
+ + [
74
+ f"(trigger) {action.describe_for_cli()}"
75
+ for action in automation.actions_on_trigger
76
+ ]
77
+ + [
78
+ f"(resolve) {action.describe_for_cli()}"
79
+ for action in automation.actions
80
+ ]
81
+ + [
82
+ f"(resolve) {action.describe_for_cli()}"
83
+ for action in automation.actions_on_resolve
84
+ ]
85
+ )
86
+ else:
87
+ actions = [action.describe_for_cli() for action in automation.actions]
88
+
89
+ table.add_row(
90
+ identifier_column,
91
+ str(automation.enabled),
92
+ automation.trigger.describe_for_cli(),
93
+ "\n".join(actions),
94
+ )
95
+
96
+ app.console.print(table)
97
+
98
+
99
+ @automations_app.command()
100
+ @requires_automations
101
+ async def inspect(id_or_name: str, yaml: bool = False, json: bool = False):
102
+ """Inspect an automation."""
103
+ async with get_client() as client:
104
+ automation = await client.find_automation(id_or_name)
105
+ if not automation:
106
+ exit_with_error(f"Automation {id_or_name!r} not found.")
107
+
108
+ if yaml:
109
+ app.console.print(
110
+ pyyaml.dump(automation.dict(json_compatible=True), sort_keys=False)
111
+ )
112
+ elif json:
113
+ app.console.print(
114
+ orjson.dumps(
115
+ automation.dict(json_compatible=True), option=orjson.OPT_INDENT_2
116
+ ).decode()
117
+ )
118
+ else:
119
+ app.console.print(Pretty(automation))
120
+
121
+
122
+ @automations_app.command(aliases=["enable"])
123
+ @requires_automations
124
+ async def resume(id_or_name: str):
125
+ """Resume an automation."""
126
+ async with get_client() as client:
127
+ automation = await client.find_automation(id_or_name)
128
+ if not automation:
129
+ exit_with_error(f"Automation {id_or_name!r} not found.")
130
+
131
+ async with get_client() as client:
132
+ await client.resume_automation(automation.id)
133
+
134
+ exit_with_success(f"Resumed automation {automation.name!r} ({automation.id})")
135
+
136
+
137
+ @automations_app.command(aliases=["disable"])
138
+ @requires_automations
139
+ async def pause(id_or_name: str):
140
+ """Pause an automation."""
141
+ async with get_client() as client:
142
+ automation = await client.find_automation(id_or_name)
143
+ if not automation:
144
+ exit_with_error(f"Automation {id_or_name!r} not found.")
145
+
146
+ async with get_client() as client:
147
+ await client.pause_automation(automation.id)
148
+
149
+ exit_with_success(f"Paused automation {automation.name!r} ({automation.id})")
150
+
151
+
152
+ @automations_app.command()
153
+ @requires_automations
154
+ async def delete(
155
+ name: Optional[str] = typer.Argument(None, help="An automation's name"),
156
+ id: Optional[str] = typer.Option(None, "--id", help="An automation's id"),
157
+ ):
158
+ """Delete an automation.
159
+
160
+ Arguments:
161
+ name: the name of the automation to delete
162
+ id: the id of the automation to delete
163
+
164
+ Examples:
165
+ $ prefect automation delete "my-automation"
166
+ $ prefect automation delete --id "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
167
+ """
168
+
169
+ async with get_client() as client:
170
+ if not id and not name:
171
+ exit_with_error("Please provide either a name or an id.")
172
+
173
+ if id:
174
+ automation = await client.read_automation(id)
175
+ if not automation:
176
+ exit_with_error(f"Automation with id {id!r} not found.")
177
+ if not typer.confirm(
178
+ (f"Are you sure you want to delete automation with id {id!r}?"),
179
+ default=False,
180
+ ):
181
+ exit_with_error("Deletion aborted.")
182
+ await client.delete_automation(id)
183
+ exit_with_success(f"Deleted automation with id {id!r}")
184
+
185
+ elif name:
186
+ automation = await client.read_automations_by_name(name=name)
187
+ if not automation:
188
+ exit_with_error(
189
+ f"Automation {name!r} not found. You can also specify an id with the `--id` flag."
190
+ )
191
+ elif len(automation) > 1:
192
+ exit_with_error(
193
+ f"Multiple automations found with name {name!r}. Please specify an id with the `--id` flag instead."
194
+ )
195
+ if not typer.confirm(
196
+ (f"Are you sure you want to delete automation with name {name!r}?"),
197
+ default=False,
198
+ ):
199
+ exit_with_error("Deletion aborted.")
200
+ await client.delete_automation(automation[0].id)
201
+ exit_with_success(f"Deleted automation with name {name!r}")