prefect-client 3.1.11__py3-none-any.whl → 3.1.13__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 (133) hide show
  1. prefect/_experimental/sla/__init__.py +0 -0
  2. prefect/_experimental/sla/client.py +92 -0
  3. prefect/_experimental/sla/objects.py +61 -0
  4. prefect/_internal/concurrency/services.py +2 -2
  5. prefect/_internal/concurrency/threads.py +6 -0
  6. prefect/_internal/retries.py +6 -3
  7. prefect/_internal/schemas/validators.py +6 -4
  8. prefect/_version.py +3 -3
  9. prefect/artifacts.py +4 -1
  10. prefect/automations.py +236 -30
  11. prefect/blocks/__init__.py +3 -3
  12. prefect/blocks/abstract.py +57 -31
  13. prefect/blocks/core.py +181 -82
  14. prefect/blocks/notifications.py +134 -73
  15. prefect/blocks/redis.py +13 -9
  16. prefect/blocks/system.py +24 -11
  17. prefect/blocks/webhook.py +7 -5
  18. prefect/cache_policies.py +23 -22
  19. prefect/client/orchestration/__init__.py +103 -2006
  20. prefect/client/orchestration/_automations/__init__.py +0 -0
  21. prefect/client/orchestration/_automations/client.py +329 -0
  22. prefect/client/orchestration/_blocks_documents/__init__.py +0 -0
  23. prefect/client/orchestration/_blocks_documents/client.py +334 -0
  24. prefect/client/orchestration/_blocks_schemas/__init__.py +0 -0
  25. prefect/client/orchestration/_blocks_schemas/client.py +200 -0
  26. prefect/client/orchestration/_blocks_types/__init__.py +0 -0
  27. prefect/client/orchestration/_blocks_types/client.py +380 -0
  28. prefect/client/orchestration/_deployments/__init__.py +0 -0
  29. prefect/client/orchestration/_deployments/client.py +1128 -0
  30. prefect/client/orchestration/_flow_runs/__init__.py +0 -0
  31. prefect/client/orchestration/_flow_runs/client.py +903 -0
  32. prefect/client/orchestration/_flows/__init__.py +0 -0
  33. prefect/client/orchestration/_flows/client.py +343 -0
  34. prefect/client/orchestration/_logs/client.py +16 -14
  35. prefect/client/schemas/__init__.py +68 -28
  36. prefect/client/schemas/objects.py +5 -5
  37. prefect/client/utilities.py +3 -3
  38. prefect/context.py +15 -1
  39. prefect/deployments/base.py +13 -4
  40. prefect/deployments/flow_runs.py +5 -1
  41. prefect/deployments/runner.py +37 -1
  42. prefect/deployments/steps/core.py +1 -1
  43. prefect/deployments/steps/pull.py +8 -3
  44. prefect/deployments/steps/utility.py +2 -2
  45. prefect/docker/docker_image.py +13 -9
  46. prefect/engine.py +33 -11
  47. prefect/events/cli/automations.py +4 -4
  48. prefect/events/clients.py +17 -14
  49. prefect/events/schemas/automations.py +12 -8
  50. prefect/events/schemas/events.py +5 -1
  51. prefect/events/worker.py +1 -1
  52. prefect/filesystems.py +7 -3
  53. prefect/flow_engine.py +64 -47
  54. prefect/flows.py +128 -74
  55. prefect/futures.py +14 -7
  56. prefect/infrastructure/provisioners/__init__.py +2 -0
  57. prefect/infrastructure/provisioners/cloud_run.py +4 -4
  58. prefect/infrastructure/provisioners/coiled.py +249 -0
  59. prefect/infrastructure/provisioners/container_instance.py +4 -3
  60. prefect/infrastructure/provisioners/ecs.py +55 -43
  61. prefect/infrastructure/provisioners/modal.py +5 -4
  62. prefect/input/actions.py +5 -1
  63. prefect/input/run_input.py +157 -43
  64. prefect/logging/configuration.py +3 -3
  65. prefect/logging/filters.py +2 -2
  66. prefect/logging/formatters.py +15 -11
  67. prefect/logging/handlers.py +24 -14
  68. prefect/logging/highlighters.py +5 -5
  69. prefect/logging/loggers.py +28 -18
  70. prefect/logging/logging.yml +1 -1
  71. prefect/main.py +3 -1
  72. prefect/results.py +166 -86
  73. prefect/runner/runner.py +38 -29
  74. prefect/runner/server.py +3 -1
  75. prefect/runner/storage.py +18 -18
  76. prefect/runner/submit.py +19 -12
  77. prefect/runtime/deployment.py +15 -8
  78. prefect/runtime/flow_run.py +19 -6
  79. prefect/runtime/task_run.py +7 -3
  80. prefect/settings/base.py +17 -7
  81. prefect/settings/legacy.py +4 -4
  82. prefect/settings/models/api.py +4 -3
  83. prefect/settings/models/cli.py +4 -3
  84. prefect/settings/models/client.py +7 -4
  85. prefect/settings/models/cloud.py +9 -3
  86. prefect/settings/models/deployments.py +4 -3
  87. prefect/settings/models/experiments.py +4 -8
  88. prefect/settings/models/flows.py +4 -3
  89. prefect/settings/models/internal.py +4 -3
  90. prefect/settings/models/logging.py +8 -6
  91. prefect/settings/models/results.py +4 -3
  92. prefect/settings/models/root.py +11 -16
  93. prefect/settings/models/runner.py +8 -5
  94. prefect/settings/models/server/api.py +6 -3
  95. prefect/settings/models/server/database.py +120 -25
  96. prefect/settings/models/server/deployments.py +4 -3
  97. prefect/settings/models/server/ephemeral.py +7 -4
  98. prefect/settings/models/server/events.py +6 -3
  99. prefect/settings/models/server/flow_run_graph.py +4 -3
  100. prefect/settings/models/server/root.py +4 -3
  101. prefect/settings/models/server/services.py +15 -12
  102. prefect/settings/models/server/tasks.py +7 -4
  103. prefect/settings/models/server/ui.py +4 -3
  104. prefect/settings/models/tasks.py +10 -5
  105. prefect/settings/models/testing.py +4 -3
  106. prefect/settings/models/worker.py +7 -4
  107. prefect/settings/profiles.py +13 -12
  108. prefect/settings/sources.py +20 -19
  109. prefect/states.py +74 -51
  110. prefect/task_engine.py +43 -33
  111. prefect/task_runners.py +85 -72
  112. prefect/task_runs.py +20 -11
  113. prefect/task_worker.py +14 -9
  114. prefect/tasks.py +36 -28
  115. prefect/telemetry/bootstrap.py +13 -9
  116. prefect/telemetry/run_telemetry.py +15 -13
  117. prefect/telemetry/services.py +4 -0
  118. prefect/transactions.py +3 -3
  119. prefect/types/__init__.py +3 -1
  120. prefect/utilities/_deprecated.py +38 -0
  121. prefect/utilities/engine.py +11 -4
  122. prefect/utilities/filesystem.py +2 -2
  123. prefect/utilities/generics.py +1 -1
  124. prefect/utilities/pydantic.py +21 -36
  125. prefect/utilities/templating.py +25 -1
  126. prefect/workers/base.py +58 -33
  127. prefect/workers/process.py +20 -15
  128. prefect/workers/server.py +4 -5
  129. {prefect_client-3.1.11.dist-info → prefect_client-3.1.13.dist-info}/METADATA +3 -3
  130. {prefect_client-3.1.11.dist-info → prefect_client-3.1.13.dist-info}/RECORD +133 -114
  131. {prefect_client-3.1.11.dist-info → prefect_client-3.1.13.dist-info}/LICENSE +0 -0
  132. {prefect_client-3.1.11.dist-info → prefect_client-3.1.13.dist-info}/WHEEL +0 -0
  133. {prefect_client-3.1.11.dist-info → prefect_client-3.1.13.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from prefect.client.orchestration.base import BaseAsyncClient, BaseClient
6
+
7
+ if TYPE_CHECKING:
8
+ from uuid import UUID
9
+
10
+ from prefect._experimental.sla.objects import SlaMergeResponse, SlaTypes
11
+
12
+
13
+ class SlaClient(BaseClient):
14
+ def apply_slas_for_deployment(
15
+ self, deployment_id: "UUID", slas: "list[SlaTypes]"
16
+ ) -> "SlaMergeResponse":
17
+ """
18
+ Applies service level agreements for a deployment. Performs matching by SLA name. If a SLA with the same name already exists, it will be updated. If a SLA with the same name does not exist, it will be created. Existing SLAs that are not in the list will be deleted.
19
+ Args:
20
+ deployment_id: The ID of the deployment to update SLAs for
21
+ slas: List of SLAs to associate with the deployment
22
+ Raises:
23
+ httpx.RequestError: if the SLAs were not updated for any reason
24
+ Returns:
25
+ SlaMergeResponse: The response from the backend, containing the names of the created, updated, and deleted SLAs
26
+ """
27
+ resource_id = f"prefect.deployment.{deployment_id}"
28
+
29
+ for sla in slas:
30
+ sla.set_deployment_id(deployment_id)
31
+
32
+ slas_spec_list = [
33
+ sla.model_dump(mode="json", exclude_unset=True) for sla in slas
34
+ ]
35
+
36
+ response = self.request(
37
+ "POST",
38
+ f"/slas/apply-resource-slas/{resource_id}",
39
+ json=slas_spec_list,
40
+ )
41
+ response.raise_for_status()
42
+
43
+ response_json = response.json()
44
+
45
+ from prefect._experimental.sla.objects import SlaMergeResponse
46
+
47
+ return SlaMergeResponse(
48
+ created=[sla.get("name") for sla in response_json.get("created")],
49
+ updated=[sla.get("name") for sla in response_json.get("updated")],
50
+ deleted=[sla.get("name") for sla in response_json.get("deleted")],
51
+ )
52
+
53
+
54
+ class SlaAsyncClient(BaseAsyncClient):
55
+ async def apply_slas_for_deployment(
56
+ self, deployment_id: "UUID", slas: "list[SlaTypes]"
57
+ ) -> "UUID":
58
+ """
59
+ Applies service level agreements for a deployment. Performs matching by SLA name. If a SLA with the same name already exists, it will be updated. If a SLA with the same name does not exist, it will be created. Existing SLAs that are not in the list will be deleted.
60
+ Args:
61
+ deployment_id: The ID of the deployment to update SLAs for
62
+ slas: List of SLAs to associate with the deployment
63
+ Raises:
64
+ httpx.RequestError: if the SLAs were not updated for any reason
65
+ Returns:
66
+ SlaMergeResponse: The response from the backend, containing the names of the created, updated, and deleted SLAs
67
+ """
68
+ resource_id = f"prefect.deployment.{deployment_id}"
69
+
70
+ for sla in slas:
71
+ sla.set_deployment_id(deployment_id)
72
+
73
+ slas_spec_list = [
74
+ sla.model_dump(mode="json", exclude_unset=True) for sla in slas
75
+ ]
76
+
77
+ response = await self.request(
78
+ "POST",
79
+ f"/slas/apply-resource-slas/{resource_id}",
80
+ json=slas_spec_list,
81
+ )
82
+ response.raise_for_status()
83
+
84
+ response_json = response.json()
85
+
86
+ from prefect._experimental.sla.objects import SlaMergeResponse
87
+
88
+ return SlaMergeResponse(
89
+ created=[sla.get("name") for sla in response_json.get("created")],
90
+ updated=[sla.get("name") for sla in response_json.get("updated")],
91
+ deleted=[sla.get("name") for sla in response_json.get("deleted")],
92
+ )
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from typing import Literal, Optional, Union
5
+ from uuid import UUID
6
+
7
+ from pydantic import Field, PrivateAttr, computed_field
8
+ from typing_extensions import Self, TypeAlias
9
+
10
+ from prefect._internal.schemas.bases import PrefectBaseModel
11
+
12
+
13
+ class ServiceLevelAgreement(PrefectBaseModel, abc.ABC):
14
+ """An ORM representation of a Service Level Agreement."""
15
+
16
+ _deployment_id: Optional[UUID] = PrivateAttr(default=None)
17
+
18
+ name: str = Field(
19
+ default=...,
20
+ description="The name of the SLA. Names must be unique on a per-deployment basis.",
21
+ )
22
+ severity: Literal["minor", "low", "moderate", "high", "critical"] = Field(
23
+ default="moderate",
24
+ description="The severity of the SLA.",
25
+ )
26
+ enabled: Optional[bool] = Field(
27
+ default=True,
28
+ description="Whether the SLA is enabled.",
29
+ )
30
+
31
+ def set_deployment_id(self, deployment_id: UUID) -> Self:
32
+ self._deployment_id = deployment_id
33
+ return self
34
+
35
+ @computed_field
36
+ @property
37
+ def owner_resource(self) -> Union[str, None]:
38
+ if self._deployment_id:
39
+ return f"prefect.deployment.{self._deployment_id}"
40
+ return None
41
+
42
+
43
+ class TimeToCompletionSla(ServiceLevelAgreement):
44
+ """An SLA that triggers when a flow run takes longer than the specified duration."""
45
+
46
+ duration: int = Field(
47
+ default=...,
48
+ description="The maximum flow run duration allowed before the SLA is violated, expressed in seconds.",
49
+ )
50
+
51
+
52
+ class SlaMergeResponse(PrefectBaseModel):
53
+ """A response object for the apply_slas_for_deployment method. Contains the names of the created, updated, and deleted SLAs."""
54
+
55
+ created: list[str]
56
+ updated: list[str]
57
+ deleted: list[str]
58
+
59
+
60
+ # Concrete SLA types
61
+ SlaTypes: TypeAlias = Union[TimeToCompletionSla]
@@ -32,7 +32,7 @@ class _QueueServiceBase(abc.ABC, Generic[T]):
32
32
  self._task: Optional[asyncio.Task[None]] = None
33
33
  self._stopped: bool = False
34
34
  self._started: bool = False
35
- self._key = hash(args)
35
+ self._key = hash((self.__class__, *args))
36
36
  self._lock = threading.Lock()
37
37
  self._queue_get_thread = WorkerThread(
38
38
  # TODO: This thread should not need to be a daemon but when it is not, it
@@ -256,7 +256,7 @@ class _QueueServiceBase(abc.ABC, Generic[T]):
256
256
  If an instance already exists with the given arguments, it will be returned.
257
257
  """
258
258
  with cls._instance_lock:
259
- key = hash(args)
259
+ key = hash((cls, *args))
260
260
  if key not in cls._instances:
261
261
  cls._instances[key] = cls._new_instance(*args)
262
262
 
@@ -2,6 +2,8 @@
2
2
  Utilities for managing worker threads.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import asyncio
6
8
  import atexit
7
9
  import concurrent.futures
@@ -197,6 +199,10 @@ class EventLoopThread(Portal):
197
199
  def running(self) -> bool:
198
200
  return not self._shutdown_event.is_set()
199
201
 
202
+ @property
203
+ def loop(self) -> asyncio.AbstractEventLoop | None:
204
+ return self._loop
205
+
200
206
  def _entrypoint(self):
201
207
  """
202
208
  Entrypoint for the thread.
@@ -29,7 +29,7 @@ def retry_async_fn(
29
29
  retry_on_exceptions: tuple[type[Exception], ...] = (Exception,),
30
30
  operation_name: Optional[str] = None,
31
31
  ) -> Callable[
32
- [Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, Optional[R]]]
32
+ [Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
33
33
  ]:
34
34
  """A decorator for retrying an async function.
35
35
 
@@ -48,9 +48,9 @@ def retry_async_fn(
48
48
 
49
49
  def decorator(
50
50
  func: Callable[P, Coroutine[Any, Any, R]],
51
- ) -> Callable[P, Coroutine[Any, Any, Optional[R]]]:
51
+ ) -> Callable[P, Coroutine[Any, Any, R]]:
52
52
  @wraps(func)
53
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Optional[R]:
53
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
54
54
  name = operation_name or func.__name__
55
55
  for attempt in range(max_attempts):
56
56
  try:
@@ -67,6 +67,9 @@ def retry_async_fn(
67
67
  f"Retrying in {delay:.2f} seconds..."
68
68
  )
69
69
  await asyncio.sleep(delay)
70
+ # Technically unreachable, but this raise helps pyright know that this function
71
+ # won't return None.
72
+ raise Exception(f"Function {name!r} failed after {max_attempts} attempts")
70
73
 
71
74
  return wrapper
72
75
 
@@ -6,6 +6,8 @@ format.
6
6
  This will be subject to consolidation and refactoring over the next few months.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import os
10
12
  import re
11
13
  import urllib.parse
@@ -627,18 +629,18 @@ def validate_name_present_on_nonanonymous_blocks(values: M) -> M:
627
629
 
628
630
 
629
631
  @overload
630
- def validate_command(v: str) -> Path:
632
+ def validate_working_dir(v: str) -> Path:
631
633
  ...
632
634
 
633
635
 
634
636
  @overload
635
- def validate_command(v: None) -> None:
637
+ def validate_working_dir(v: None) -> None:
636
638
  ...
637
639
 
638
640
 
639
- def validate_command(v: Optional[str]) -> Optional[Path]:
641
+ def validate_working_dir(v: Optional[Path | str]) -> Optional[Path]:
640
642
  """Make sure that the working directory is formatted for the current platform."""
641
- if v is not None:
643
+ if isinstance(v, str):
642
644
  return relative_path_to_current_platform(v)
643
645
  return v
644
646
 
prefect/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-01-02T13:11:17-0600",
11
+ "date": "2025-01-17T08:46:53-0800",
12
12
  "dirty": true,
13
13
  "error": null,
14
- "full-revisionid": "e448bd3462d7c2580427624f426d81cf6e9a7ff1",
15
- "version": "3.1.11"
14
+ "full-revisionid": "16e85ce3c281778f5ab6487a73377eed63bcac8b",
15
+ "version": "3.1.13"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
prefect/artifacts.py CHANGED
@@ -20,7 +20,10 @@ from prefect.logging.loggers import get_logger
20
20
  from prefect.utilities.asyncutils import sync_compatible
21
21
  from prefect.utilities.context import get_task_and_flow_run_ids
22
22
 
23
- logger = get_logger("artifacts")
23
+ if TYPE_CHECKING:
24
+ import logging
25
+
26
+ logger: "logging.Logger" = get_logger("artifacts")
24
27
 
25
28
  if TYPE_CHECKING:
26
29
  from prefect.client.orchestration import PrefectClient
prefect/automations.py CHANGED
@@ -4,6 +4,7 @@ from uuid import UUID
4
4
  from pydantic import Field
5
5
  from typing_extensions import Self
6
6
 
7
+ from prefect._internal.compatibility.async_dispatch import async_dispatch
7
8
  from prefect.client.orchestration import get_client
8
9
  from prefect.events.actions import (
9
10
  CallWebhook,
@@ -39,7 +40,6 @@ from prefect.events.schemas.automations import (
39
40
  Trigger,
40
41
  )
41
42
  from prefect.exceptions import PrefectHTTPStatusError
42
- from prefect.utilities.asyncutils import sync_compatible
43
43
 
44
44
  __all__ = [
45
45
  "AutomationCore",
@@ -78,11 +78,13 @@ __all__ = [
78
78
  class Automation(AutomationCore):
79
79
  id: Optional[UUID] = Field(default=None, description="The ID of this automation")
80
80
 
81
- @sync_compatible
82
- async def create(self: Self) -> Self:
81
+ async def acreate(self: Self) -> Self:
83
82
  """
84
- Create a new automation.
83
+ Asynchronously create a new automation.
84
+
85
+ Examples:
85
86
 
87
+ ```python
86
88
  auto_to_create = Automation(
87
89
  name="woodchonk",
88
90
  trigger=EventTrigger(
@@ -97,20 +99,55 @@ class Automation(AutomationCore):
97
99
  ),
98
100
  actions=[CancelFlowRun()]
99
101
  )
100
- created_automation = auto_to_create.create()
102
+ created_automation = await auto_to_create.acreate()
103
+ ```
101
104
  """
102
105
  async with get_client() as client:
103
106
  automation = AutomationCore(**self.model_dump(exclude={"id"}))
104
107
  self.id = await client.create_automation(automation=automation)
105
108
  return self
106
109
 
107
- @sync_compatible
108
- async def update(self: Self):
110
+ @async_dispatch(acreate)
111
+ def create(self: Self) -> Self:
112
+ """
113
+ Create a new automation.
114
+
115
+ Examples:
116
+
117
+ ```python
118
+ auto_to_create = Automation(
119
+ name="woodchonk",
120
+ trigger=EventTrigger(
121
+ expect={"animal.walked"},
122
+ match={
123
+ "genus": "Marmota",
124
+ "species": "monax",
125
+ },
126
+ posture="Reactive",
127
+ threshold=3,
128
+ within=timedelta(seconds=10),
129
+ ),
130
+ actions=[CancelFlowRun()]
131
+ )
132
+ created_automation = auto_to_create.create()
133
+ ```
134
+ """
135
+ with get_client(sync_client=True) as client:
136
+ automation = AutomationCore(**self.model_dump(exclude={"id"}))
137
+ self.id = client.create_automation(automation=automation)
138
+ return self
139
+
140
+ async def aupdate(self: Self) -> None:
109
141
  """
110
142
  Updates an existing automation.
143
+
144
+ Examples:
145
+
146
+ ```python
111
147
  auto = Automation.read(id=123)
112
148
  auto.name = "new name"
113
149
  auto.update()
150
+ ```
114
151
  """
115
152
  assert self.id is not None
116
153
  async with get_client() as client:
@@ -119,28 +156,51 @@ class Automation(AutomationCore):
119
156
  )
120
157
  await client.update_automation(automation_id=self.id, automation=automation)
121
158
 
159
+ @async_dispatch(aupdate)
160
+ def update(self: Self):
161
+ """
162
+ Updates an existing automation.
163
+
164
+ Examples:
165
+
166
+
167
+ ```python
168
+ auto = Automation.read(id=123)
169
+ auto.name = "new name"
170
+ auto.update()
171
+ ```
172
+ """
173
+ assert self.id is not None
174
+ with get_client(sync_client=True) as client:
175
+ automation = AutomationCore(
176
+ **self.model_dump(exclude={"id", "owner_resource"})
177
+ )
178
+ client.update_automation(automation_id=self.id, automation=automation)
179
+
122
180
  @overload
123
181
  @classmethod
124
- async def read(cls, id: UUID, name: Optional[str] = ...) -> Self:
182
+ async def aread(cls, id: UUID, name: Optional[str] = ...) -> Self:
125
183
  ...
126
184
 
127
185
  @overload
128
186
  @classmethod
129
- async def read(cls, id: None = None, name: str = ...) -> Optional[Self]:
187
+ async def aread(cls, id: None = None, name: str = ...) -> Self:
130
188
  ...
131
189
 
132
190
  @classmethod
133
- @sync_compatible
134
- async def read(
135
- cls, id: Optional[UUID] = None, name: Optional[str] = None
136
- ) -> Optional[Self]:
191
+ async def aread(cls, id: Optional[UUID] = None, name: Optional[str] = None) -> Self:
137
192
  """
138
- Read an automation by ID or name.
139
- automation = Automation.read(name="woodchonk")
193
+ Asynchronously read an automation by ID or name.
140
194
 
141
- or
195
+ Examples:
142
196
 
143
- automation = Automation.read(id=UUID("b3514963-02b1-47a5-93d1-6eeb131041cb"))
197
+ ```python
198
+ automation = await Automation.aread(name="woodchonk")
199
+ ```
200
+
201
+ ```python
202
+ automation = await Automation.aread(id=UUID("b3514963-02b1-47a5-93d1-6eeb131041cb"))
203
+ ```
144
204
  """
145
205
  if id and name:
146
206
  raise ValueError("Only one of id or name can be provided")
@@ -162,15 +222,68 @@ class Automation(AutomationCore):
162
222
  assert name is not None
163
223
  automation = await client.read_automations_by_name(name=name)
164
224
  if len(automation) > 0:
165
- return cls(**automation[0].model_dump()) if automation else None
166
- else:
167
- raise ValueError(f"Automation with name {name!r} not found")
225
+ return cls(**automation[0].model_dump())
226
+ raise ValueError(f"Automation with name {name!r} not found")
168
227
 
169
- @sync_compatible
170
- async def delete(self: Self) -> bool:
228
+ @overload
229
+ @classmethod
230
+ async def read(cls, id: UUID, name: Optional[str] = ...) -> Self:
231
+ ...
232
+
233
+ @overload
234
+ @classmethod
235
+ async def read(cls, id: None = None, name: str = ...) -> Self:
236
+ ...
237
+
238
+ @classmethod
239
+ @async_dispatch(aread)
240
+ def read(cls, id: Optional[UUID] = None, name: Optional[str] = None) -> Self:
171
241
  """
242
+ Read an automation by ID or name.
243
+
244
+ Examples:
245
+
246
+ ```python
247
+ automation = Automation.read(name="woodchonk")
248
+ ```
249
+
250
+ ```python
251
+ automation = Automation.read(id=UUID("b3514963-02b1-47a5-93d1-6eeb131041cb"))
252
+ ```
253
+ """
254
+ if id and name:
255
+ raise ValueError("Only one of id or name can be provided")
256
+ if not id and not name:
257
+ raise ValueError("One of id or name must be provided")
258
+ with get_client(sync_client=True) as client:
259
+ if id:
260
+ try:
261
+ automation = client.read_automation(automation_id=id)
262
+ except PrefectHTTPStatusError as exc:
263
+ if exc.response.status_code == 404:
264
+ raise ValueError(f"Automation with ID {id!r} not found")
265
+ raise
266
+ if automation is None:
267
+ raise ValueError(f"Automation with ID {id!r} not found")
268
+ return cls(**automation.model_dump())
269
+ else:
270
+ if TYPE_CHECKING:
271
+ assert name is not None
272
+ automation = client.read_automations_by_name(name=name)
273
+ if len(automation) > 0:
274
+ return cls(**automation[0].model_dump())
275
+ raise ValueError(f"Automation with name {name!r} not found")
276
+
277
+ async def adelete(self: Self) -> bool:
278
+ """
279
+ Asynchronously delete an automation.
280
+
281
+ Examples:
282
+
283
+ ```python
172
284
  auto = Automation.read(id = 123)
173
- auto.delete()
285
+ await auto.adelete()
286
+ ```
174
287
  """
175
288
  if self.id is None:
176
289
  raise ValueError("Can't delete an automation without an id")
@@ -184,38 +297,131 @@ class Automation(AutomationCore):
184
297
  return False
185
298
  raise
186
299
 
187
- @sync_compatible
188
- async def disable(self: Self) -> bool:
300
+ @async_dispatch(adelete)
301
+ def delete(self: Self) -> bool:
302
+ """
303
+ Delete an automation.
304
+
305
+ Examples:
306
+
307
+ ```python
308
+ auto = Automation.read(id = 123)
309
+ auto.delete()
310
+ ```
311
+ """
312
+ if self.id is None:
313
+ raise ValueError("Can't delete an automation without an id")
314
+
315
+ with get_client(sync_client=True) as client:
316
+ try:
317
+ client.delete_automation(self.id)
318
+ return True
319
+ except PrefectHTTPStatusError as exc:
320
+ if exc.response.status_code == 404:
321
+ return False
322
+ raise
323
+
324
+ async def adisable(self: Self) -> bool:
325
+ """
326
+ Asynchronously disable an automation.
327
+
328
+ Raises:
329
+ ValueError: If the automation does not have an id
330
+ PrefectHTTPStatusError: If the automation cannot be disabled
331
+
332
+ Example:
333
+ ```python
334
+ auto = await Automation.aread(id = 123)
335
+ await auto.adisable()
336
+ ```
337
+ """
338
+ if self.id is None:
339
+ raise ValueError("Can't disable an automation without an id")
340
+
341
+ async with get_client() as client:
342
+ try:
343
+ await client.pause_automation(self.id)
344
+ return True
345
+ except PrefectHTTPStatusError as exc:
346
+ if exc.response.status_code == 404:
347
+ return False
348
+ raise
349
+
350
+ @async_dispatch(adisable)
351
+ def disable(self: Self) -> bool:
189
352
  """
190
353
  Disable an automation.
354
+
355
+
356
+ Raises:
357
+ ValueError: If the automation does not have an id
358
+ PrefectHTTPStatusError: If the automation cannot be disabled
359
+
360
+ Example:
361
+ ```python
191
362
  auto = Automation.read(id = 123)
192
363
  auto.disable()
364
+ ```
193
365
  """
194
366
  if self.id is None:
195
367
  raise ValueError("Can't disable an automation without an id")
196
368
 
369
+ with get_client(sync_client=True) as client:
370
+ try:
371
+ client.pause_automation(self.id)
372
+ return True
373
+ except PrefectHTTPStatusError as exc:
374
+ if exc.response.status_code == 404:
375
+ return False
376
+ raise
377
+
378
+ async def aenable(self: Self) -> bool:
379
+ """
380
+ Asynchronously enable an automation.
381
+
382
+ Raises:
383
+ ValueError: If the automation does not have an id
384
+ PrefectHTTPStatusError: If the automation cannot be enabled
385
+
386
+ Example:
387
+ ```python
388
+ auto = await Automation.aread(id = 123)
389
+ await auto.aenable()
390
+ ```
391
+ """
392
+ if self.id is None:
393
+ raise ValueError("Can't enable an automation without an id")
394
+
197
395
  async with get_client() as client:
198
396
  try:
199
- await client.pause_automation(self.id)
397
+ await client.resume_automation(self.id)
200
398
  return True
201
399
  except PrefectHTTPStatusError as exc:
202
400
  if exc.response.status_code == 404:
203
401
  return False
204
402
  raise
205
403
 
206
- @sync_compatible
207
- async def enable(self: Self) -> bool:
404
+ @async_dispatch(aenable)
405
+ def enable(self: Self) -> bool:
208
406
  """
209
407
  Enable an automation.
408
+
409
+ Raises:
410
+ ValueError: If the automation does not have an id
411
+ PrefectHTTPStatusError: If the automation cannot be enabled
412
+
413
+ Example:
414
+ ```python
210
415
  auto = Automation.read(id = 123)
211
416
  auto.enable()
417
+ ```
212
418
  """
213
419
  if self.id is None:
214
420
  raise ValueError("Can't enable an automation without an id")
215
421
 
216
- async with get_client() as client:
422
+ with get_client(sync_client=True) as client:
217
423
  try:
218
- await client.resume_automation(self.id)
424
+ client.resume_automation(self.id)
219
425
  return True
220
426
  except PrefectHTTPStatusError as exc:
221
427
  if exc.response.status_code == 404:
@@ -1,7 +1,7 @@
1
1
  # ensure core blocks are registered
2
2
 
3
- import prefect.blocks.notifications
4
- import prefect.blocks.system
5
- import prefect.blocks.webhook
3
+ import prefect.blocks.notifications as notifications
4
+ import prefect.blocks.system as system
5
+ import prefect.blocks.webhook as webhook
6
6
 
7
7
  __all__ = ["notifications", "system", "webhook"]