prefect-client 3.1.11__py3-none-any.whl → 3.1.12__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.
- prefect/_experimental/sla/__init__.py +0 -0
- prefect/_experimental/sla/client.py +66 -0
- prefect/_experimental/sla/objects.py +53 -0
- prefect/_version.py +3 -3
- prefect/automations.py +236 -30
- prefect/blocks/__init__.py +3 -3
- prefect/blocks/abstract.py +53 -30
- prefect/blocks/core.py +181 -82
- prefect/blocks/notifications.py +133 -73
- prefect/blocks/redis.py +13 -9
- prefect/blocks/system.py +24 -11
- prefect/blocks/webhook.py +7 -5
- prefect/cache_policies.py +3 -2
- prefect/client/orchestration/__init__.py +103 -2006
- prefect/client/orchestration/_automations/__init__.py +0 -0
- prefect/client/orchestration/_automations/client.py +329 -0
- prefect/client/orchestration/_blocks_documents/__init__.py +0 -0
- prefect/client/orchestration/_blocks_documents/client.py +334 -0
- prefect/client/orchestration/_blocks_schemas/__init__.py +0 -0
- prefect/client/orchestration/_blocks_schemas/client.py +200 -0
- prefect/client/orchestration/_blocks_types/__init__.py +0 -0
- prefect/client/orchestration/_blocks_types/client.py +380 -0
- prefect/client/orchestration/_deployments/__init__.py +0 -0
- prefect/client/orchestration/_deployments/client.py +1128 -0
- prefect/client/orchestration/_flow_runs/__init__.py +0 -0
- prefect/client/orchestration/_flow_runs/client.py +903 -0
- prefect/client/orchestration/_flows/__init__.py +0 -0
- prefect/client/orchestration/_flows/client.py +343 -0
- prefect/client/orchestration/_logs/client.py +16 -14
- prefect/client/schemas/__init__.py +68 -28
- prefect/client/schemas/objects.py +5 -5
- prefect/context.py +15 -1
- prefect/deployments/base.py +6 -0
- prefect/deployments/runner.py +42 -1
- prefect/engine.py +17 -4
- prefect/filesystems.py +6 -2
- prefect/flow_engine.py +47 -38
- prefect/flows.py +10 -1
- prefect/logging/logging.yml +1 -1
- prefect/runner/runner.py +4 -2
- prefect/settings/models/cloud.py +5 -0
- prefect/settings/models/experiments.py +0 -5
- prefect/states.py +57 -38
- prefect/task_runners.py +56 -55
- prefect/task_worker.py +2 -2
- prefect/tasks.py +6 -4
- prefect/telemetry/bootstrap.py +10 -9
- prefect/telemetry/services.py +4 -0
- prefect/utilities/templating.py +25 -1
- prefect/workers/base.py +6 -3
- prefect/workers/process.py +1 -1
- {prefect_client-3.1.11.dist-info → prefect_client-3.1.12.dist-info}/METADATA +2 -2
- {prefect_client-3.1.11.dist-info → prefect_client-3.1.12.dist-info}/RECORD +56 -39
- {prefect_client-3.1.11.dist-info → prefect_client-3.1.12.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.11.dist-info → prefect_client-3.1.12.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.11.dist-info → prefect_client-3.1.12.dist-info}/top_level.txt +0 -0
File without changes
|
@@ -0,0 +1,66 @@
|
|
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 SlaTypes
|
11
|
+
|
12
|
+
|
13
|
+
class SlaClient(BaseClient):
|
14
|
+
def create_sla(self, sla: "SlaTypes") -> "UUID":
|
15
|
+
"""
|
16
|
+
Creates a service level agreement.
|
17
|
+
Args:
|
18
|
+
sla: The SLA to create. Must have a deployment ID set.
|
19
|
+
Raises:
|
20
|
+
httpx.RequestError: if the SLA was not created for any reason
|
21
|
+
Returns:
|
22
|
+
the ID of the SLA in the backend
|
23
|
+
"""
|
24
|
+
if not sla.owner_resource:
|
25
|
+
raise ValueError(
|
26
|
+
"Deployment ID is not set. Please set using `set_deployment_id`."
|
27
|
+
)
|
28
|
+
|
29
|
+
response = self.request(
|
30
|
+
"POST",
|
31
|
+
"/slas/",
|
32
|
+
json=sla.model_dump(mode="json", exclude_unset=True),
|
33
|
+
)
|
34
|
+
response.raise_for_status()
|
35
|
+
|
36
|
+
from uuid import UUID
|
37
|
+
|
38
|
+
return UUID(response.json().get("id"))
|
39
|
+
|
40
|
+
|
41
|
+
class SlaAsyncClient(BaseAsyncClient):
|
42
|
+
async def create_sla(self, sla: "SlaTypes") -> "UUID":
|
43
|
+
"""
|
44
|
+
Creates a service level agreement.
|
45
|
+
Args:
|
46
|
+
sla: The SLA to create. Must have a deployment ID set.
|
47
|
+
Raises:
|
48
|
+
httpx.RequestError: if the SLA was not created for any reason
|
49
|
+
Returns:
|
50
|
+
the ID of the SLA in the backend
|
51
|
+
"""
|
52
|
+
if not sla.owner_resource:
|
53
|
+
raise ValueError(
|
54
|
+
"Deployment ID is not set. Please set using `set_deployment_id`."
|
55
|
+
)
|
56
|
+
|
57
|
+
response = await self.request(
|
58
|
+
"POST",
|
59
|
+
"/slas/",
|
60
|
+
json=sla.model_dump(mode="json", exclude_unset=True),
|
61
|
+
)
|
62
|
+
response.raise_for_status()
|
63
|
+
|
64
|
+
from uuid import UUID
|
65
|
+
|
66
|
+
return UUID(response.json().get("id"))
|
@@ -0,0 +1,53 @@
|
|
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 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):
|
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
|
+
# Concrete SLA types
|
53
|
+
SlaTypes: TypeAlias = Union[TimeToCompletionSla]
|
prefect/_version.py
CHANGED
@@ -8,11 +8,11 @@ import json
|
|
8
8
|
|
9
9
|
version_json = '''
|
10
10
|
{
|
11
|
-
"date": "2025-01-
|
11
|
+
"date": "2025-01-09T10:09:15-0800",
|
12
12
|
"dirty": true,
|
13
13
|
"error": null,
|
14
|
-
"full-revisionid": "
|
15
|
-
"version": "3.1.
|
14
|
+
"full-revisionid": "e299e5a781867735d62685e7c190b5d100a28b62",
|
15
|
+
"version": "3.1.12"
|
16
16
|
}
|
17
17
|
''' # END VERSION_JSON
|
18
18
|
|
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
|
-
|
82
|
-
async def create(self: Self) -> Self:
|
81
|
+
async def acreate(self: Self) -> Self:
|
83
82
|
"""
|
84
|
-
|
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.
|
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
|
-
@
|
108
|
-
|
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):
|
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
|
182
|
+
async def aread(cls, id: UUID, name: Optional[str] = ...) -> Self:
|
125
183
|
...
|
126
184
|
|
127
185
|
@overload
|
128
186
|
@classmethod
|
129
|
-
async def
|
187
|
+
async def aread(cls, id: None = None, name: str = ...) -> Self:
|
130
188
|
...
|
131
189
|
|
132
190
|
@classmethod
|
133
|
-
|
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
|
-
|
139
|
-
automation = Automation.read(name="woodchonk")
|
193
|
+
Asynchronously read an automation by ID or name.
|
140
194
|
|
141
|
-
|
195
|
+
Examples:
|
142
196
|
|
143
|
-
|
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())
|
166
|
-
|
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
|
-
@
|
170
|
-
|
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.
|
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
|
-
@
|
188
|
-
|
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.
|
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
|
-
@
|
207
|
-
|
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
|
-
|
422
|
+
with get_client(sync_client=True) as client:
|
217
423
|
try:
|
218
|
-
|
424
|
+
client.resume_automation(self.id)
|
219
425
|
return True
|
220
426
|
except PrefectHTTPStatusError as exc:
|
221
427
|
if exc.response.status_code == 404:
|
prefect/blocks/__init__.py
CHANGED
@@ -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"]
|