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/events/clients.py CHANGED
@@ -7,17 +7,19 @@ from typing import (
7
7
  ClassVar,
8
8
  Dict,
9
9
  List,
10
- Mapping,
10
+ MutableMapping,
11
11
  Optional,
12
12
  Tuple,
13
13
  Type,
14
14
  )
15
15
  from uuid import UUID
16
16
 
17
+ import httpx
17
18
  import orjson
18
19
  import pendulum
19
20
  from cachetools import TTLCache
20
21
  from typing_extensions import Self
22
+ from websockets import Subprotocol
21
23
  from websockets.client import WebSocketClientProtocol, connect
22
24
  from websockets.exceptions import (
23
25
  ConnectionClosed,
@@ -25,9 +27,15 @@ from websockets.exceptions import (
25
27
  ConnectionClosedOK,
26
28
  )
27
29
 
30
+ from prefect.client.base import PrefectHttpxClient
28
31
  from prefect.events import Event
29
32
  from prefect.logging import get_logger
30
- from prefect.settings import PREFECT_API_KEY, PREFECT_API_URL
33
+ from prefect.settings import (
34
+ PREFECT_API_KEY,
35
+ PREFECT_API_URL,
36
+ PREFECT_CLOUD_API_URL,
37
+ PREFECT_EXPERIMENTAL_EVENTS,
38
+ )
31
39
 
32
40
  if TYPE_CHECKING:
33
41
  from prefect.events.filters import EventFilter
@@ -35,6 +43,53 @@ if TYPE_CHECKING:
35
43
  logger = get_logger(__name__)
36
44
 
37
45
 
46
+ def get_events_client(
47
+ reconnection_attempts: int = 10,
48
+ checkpoint_every: int = 20,
49
+ ) -> "EventsClient":
50
+ api_url = PREFECT_API_URL.value()
51
+ if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
52
+ return PrefectCloudEventsClient(
53
+ reconnection_attempts=reconnection_attempts,
54
+ checkpoint_every=checkpoint_every,
55
+ )
56
+ elif PREFECT_EXPERIMENTAL_EVENTS:
57
+ if PREFECT_API_URL:
58
+ return PrefectEventsClient(
59
+ reconnection_attempts=reconnection_attempts,
60
+ checkpoint_every=checkpoint_every,
61
+ )
62
+ else:
63
+ return PrefectEphemeralEventsClient()
64
+
65
+ raise RuntimeError(
66
+ "The current server and client configuration does not support "
67
+ "events. Enable experimental events support with the "
68
+ "PREFECT_EXPERIMENTAL_EVENTS setting."
69
+ )
70
+
71
+
72
+ def get_events_subscriber(
73
+ filter: Optional["EventFilter"] = None,
74
+ reconnection_attempts: int = 10,
75
+ ) -> "PrefectEventSubscriber":
76
+ api_url = PREFECT_API_URL.value()
77
+ if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
78
+ return PrefectCloudEventSubscriber(
79
+ filter=filter, reconnection_attempts=reconnection_attempts
80
+ )
81
+ elif PREFECT_EXPERIMENTAL_EVENTS:
82
+ return PrefectEventSubscriber(
83
+ filter=filter, reconnection_attempts=reconnection_attempts
84
+ )
85
+
86
+ raise RuntimeError(
87
+ "The current server and client configuration does not support "
88
+ "events. Enable experimental events support with the "
89
+ "PREFECT_EXPERIMENTAL_EVENTS setting."
90
+ )
91
+
92
+
38
93
  class EventsClient(abc.ABC):
39
94
  """The abstract interface for all Prefect Events clients"""
40
95
 
@@ -51,7 +106,7 @@ class EventsClient(abc.ABC):
51
106
  async def _emit(self, event: Event) -> None: # pragma: no cover
52
107
  ...
53
108
 
54
- async def __aenter__(self) -> "EventsClient":
109
+ async def __aenter__(self) -> Self:
55
110
  self._in_context = True
56
111
  return self
57
112
 
@@ -99,7 +154,7 @@ class AssertingEventsClient(EventsClient):
99
154
  async def _emit(self, event: Event) -> None:
100
155
  self.events.append(event)
101
156
 
102
- async def __aenter__(self) -> "AssertingEventsClient":
157
+ async def __aenter__(self) -> Self:
103
158
  await super().__aenter__()
104
159
  self.events = []
105
160
  return self
@@ -119,6 +174,52 @@ def _get_api_url_and_key(
119
174
  return api_url, api_key
120
175
 
121
176
 
177
+ class PrefectEphemeralEventsClient(EventsClient):
178
+ """A Prefect Events client that sends events to an ephemeral Prefect server"""
179
+
180
+ def __init__(self):
181
+ if not PREFECT_EXPERIMENTAL_EVENTS:
182
+ raise ValueError(
183
+ "PrefectEphemeralEventsClient can only be used when "
184
+ "PREFECT_EXPERIMENTAL_EVENTS is set to True"
185
+ )
186
+ if PREFECT_API_KEY.value():
187
+ raise ValueError(
188
+ "PrefectEphemeralEventsClient cannot be used when PREFECT_API_KEY is set."
189
+ " Please use PrefectEventsClient or PrefectCloudEventsClient instead."
190
+ )
191
+ from prefect.server.api.server import create_app
192
+
193
+ app = create_app()
194
+
195
+ self._http_client = PrefectHttpxClient(
196
+ transport=httpx.ASGITransport(app=app, raise_app_exceptions=False),
197
+ base_url="http://ephemeral-prefect/api",
198
+ enable_csrf_support=False,
199
+ )
200
+
201
+ async def __aenter__(self) -> Self:
202
+ await super().__aenter__()
203
+ await self._http_client.__aenter__()
204
+ return self
205
+
206
+ async def __aexit__(
207
+ self,
208
+ exc_type: Optional[Type[Exception]],
209
+ exc_val: Optional[Exception],
210
+ exc_tb: Optional[TracebackType],
211
+ ) -> None:
212
+ self._websocket = None
213
+ await self._http_client.__aexit__(exc_type, exc_val, exc_tb)
214
+ return await super().__aexit__(exc_type, exc_val, exc_tb)
215
+
216
+ async def _emit(self, event: Event) -> None:
217
+ await self._http_client.post(
218
+ "/events",
219
+ json=[event.dict(json_compatible=True)],
220
+ )
221
+
222
+
122
223
  class PrefectEventsClient(EventsClient):
123
224
  """A Prefect Events client that streams events to a Prefect server"""
124
225
 
@@ -127,7 +228,7 @@ class PrefectEventsClient(EventsClient):
127
228
 
128
229
  def __init__(
129
230
  self,
130
- api_url: str = None,
231
+ api_url: Optional[str] = None,
131
232
  reconnection_attempts: int = 10,
132
233
  checkpoint_every: int = 20,
133
234
  ):
@@ -243,8 +344,8 @@ class PrefectCloudEventsClient(PrefectEventsClient):
243
344
 
244
345
  def __init__(
245
346
  self,
246
- api_url: str = None,
247
- api_key: str = None,
347
+ api_url: Optional[str] = None,
348
+ api_key: Optional[str] = None,
248
349
  reconnection_attempts: int = 10,
249
350
  checkpoint_every: int = 20,
250
351
  ):
@@ -274,18 +375,18 @@ SEEN_EVENTS_SIZE = 500_000
274
375
  SEEN_EVENTS_TTL = 120
275
376
 
276
377
 
277
- class PrefectCloudEventSubscriber:
378
+ class PrefectEventSubscriber:
278
379
  """
279
380
  Subscribes to a Prefect Cloud event stream, yielding events as they occur.
280
381
 
281
382
  Example:
282
383
 
283
- from prefect.events.clients import PrefectCloudEventSubscriber
384
+ from prefect.events.clients import PrefectEventSubscriber
284
385
  from prefect.events.filters import EventFilter, EventNameFilter
285
386
 
286
387
  filter = EventFilter(event=EventNameFilter(prefix=["prefect.flow-run."]))
287
388
 
288
- async with PrefectCloudEventSubscriber(api_url, api_key, filter) as subscriber:
389
+ async with PrefectEventSubscriber(filter=filter) as subscriber:
289
390
  async for event in subscriber:
290
391
  print(event.occurred, event.resource.id, event.event)
291
392
 
@@ -293,13 +394,14 @@ class PrefectCloudEventSubscriber:
293
394
 
294
395
  _websocket: Optional[WebSocketClientProtocol]
295
396
  _filter: "EventFilter"
296
- _seen_events: Mapping[UUID, bool]
397
+ _seen_events: MutableMapping[UUID, bool]
398
+
399
+ _api_key: Optional[str]
297
400
 
298
401
  def __init__(
299
402
  self,
300
- api_url: str = None,
301
- api_key: str = None,
302
- filter: "EventFilter" = None,
403
+ api_url: Optional[str] = None,
404
+ filter: Optional["EventFilter"] = None,
303
405
  reconnection_attempts: int = 10,
304
406
  ):
305
407
  """
@@ -309,11 +411,13 @@ class PrefectCloudEventSubscriber:
309
411
  reconnection_attempts: When the client is disconnected, how many times
310
412
  the client should attempt to reconnect
311
413
  """
312
- api_url, api_key = _get_api_url_and_key(api_url, api_key)
414
+ if not api_url:
415
+ api_url = PREFECT_API_URL.value()
416
+ self._api_key = None
313
417
 
314
418
  from prefect.events.filters import EventFilter
315
419
 
316
- self._filter = filter or EventFilter()
420
+ self._filter = filter or EventFilter() # type: ignore[call-arg]
317
421
  self._seen_events = TTLCache(maxsize=SEEN_EVENTS_SIZE, ttl=SEEN_EVENTS_TTL)
318
422
 
319
423
  socket_url = (
@@ -324,17 +428,16 @@ class PrefectCloudEventSubscriber:
324
428
 
325
429
  logger.debug("Connecting to %s", socket_url)
326
430
 
327
- self._api_key = api_key
328
431
  self._connect = connect(
329
432
  socket_url,
330
- subprotocols=["prefect"],
433
+ subprotocols=[Subprotocol("prefect")],
331
434
  )
332
435
  self._websocket = None
333
436
  self._reconnection_attempts = reconnection_attempts
334
437
  if self._reconnection_attempts < 0:
335
438
  raise ValueError("reconnection_attempts must be a non-negative integer")
336
439
 
337
- async def __aenter__(self) -> "PrefectCloudEventSubscriber":
440
+ async def __aenter__(self) -> Self:
338
441
  # Don't handle any errors in the initial connection, because these are most
339
442
  # likely a permission or configuration issue that should propagate
340
443
  await self._reconnect()
@@ -398,7 +501,7 @@ class PrefectCloudEventSubscriber:
398
501
  self._websocket = None
399
502
  await self._connect.__aexit__(exc_type, exc_val, exc_tb)
400
503
 
401
- def __aiter__(self) -> "PrefectCloudEventSubscriber":
504
+ def __aiter__(self) -> Self:
402
505
  return self
403
506
 
404
507
  async def __anext__(self) -> Event:
@@ -416,7 +519,7 @@ class PrefectCloudEventSubscriber:
416
519
 
417
520
  while True:
418
521
  message = orjson.loads(await self._websocket.recv())
419
- event = Event.parse_obj(message["event"])
522
+ event: Event = Event.parse_obj(message["event"])
420
523
 
421
524
  if event.id in self._seen_events:
422
525
  continue
@@ -441,3 +544,58 @@ class PrefectCloudEventSubscriber:
441
544
  # a standard load balancer timeout, but after that, just take a
442
545
  # beat to let things come back around.
443
546
  await asyncio.sleep(1)
547
+ raise StopAsyncIteration
548
+
549
+
550
+ class PrefectCloudEventSubscriber(PrefectEventSubscriber):
551
+ def __init__(
552
+ self,
553
+ api_url: Optional[str] = None,
554
+ api_key: Optional[str] = None,
555
+ filter: Optional["EventFilter"] = None,
556
+ reconnection_attempts: int = 10,
557
+ ):
558
+ """
559
+ Args:
560
+ api_url: The base URL for a Prefect Cloud workspace
561
+ api_key: The API of an actor with the manage_events scope
562
+ reconnection_attempts: When the client is disconnected, how many times
563
+ the client should attempt to reconnect
564
+ """
565
+ api_url, api_key = _get_api_url_and_key(api_url, api_key)
566
+
567
+ super().__init__(
568
+ api_url=api_url,
569
+ filter=filter,
570
+ reconnection_attempts=reconnection_attempts,
571
+ )
572
+
573
+ self._api_key = api_key
574
+
575
+
576
+ class PrefectCloudAccountEventSubscriber(PrefectCloudEventSubscriber):
577
+ def __init__(
578
+ self,
579
+ api_url: Optional[str] = None,
580
+ api_key: Optional[str] = None,
581
+ filter: Optional["EventFilter"] = None,
582
+ reconnection_attempts: int = 10,
583
+ ):
584
+ """
585
+ Args:
586
+ api_url: The base URL for a Prefect Cloud workspace
587
+ api_key: The API of an actor with the manage_events scope
588
+ reconnection_attempts: When the client is disconnected, how many times
589
+ the client should attempt to reconnect
590
+ """
591
+ api_url, api_key = _get_api_url_and_key(api_url, api_key)
592
+
593
+ account_api_url, _, _ = api_url.partition("/workspaces/")
594
+
595
+ super().__init__(
596
+ api_url=account_api_url,
597
+ filter=filter,
598
+ reconnection_attempts=reconnection_attempts,
599
+ )
600
+
601
+ self._api_key = api_key
prefect/events/filters.py CHANGED
@@ -13,7 +13,34 @@ from .schemas.events import Event, Resource, ResourceSpecification
13
13
  if HAS_PYDANTIC_V2:
14
14
  from pydantic.v1 import Field, PrivateAttr
15
15
  else:
16
- from pydantic import Field, PrivateAttr
16
+ from pydantic import Field, PrivateAttr # type: ignore
17
+
18
+
19
+ class AutomationFilterCreated(PrefectBaseModel):
20
+ """Filter by `Automation.created`."""
21
+
22
+ before_: Optional[DateTimeTZ] = Field(
23
+ default=None,
24
+ description="Only include automations created before this datetime",
25
+ )
26
+
27
+
28
+ class AutomationFilterName(PrefectBaseModel):
29
+ """Filter by `Automation.created`."""
30
+
31
+ any_: Optional[List[str]] = Field(
32
+ default=None,
33
+ description="Only include automations with names that match any of these strings",
34
+ )
35
+
36
+
37
+ class AutomationFilter(PrefectBaseModel):
38
+ name: Optional[AutomationFilterName] = Field(
39
+ default=None, description="Filter criteria for `Automation.name`"
40
+ )
41
+ created: Optional[AutomationFilterCreated] = Field(
42
+ default=None, description="Filter criteria for `Automation.created`"
43
+ )
17
44
 
18
45
 
19
46
  class EventDataFilter(PrefectBaseModel, extra="forbid"):
@@ -203,7 +230,7 @@ class EventOrder(AutoEnum):
203
230
 
204
231
  class EventFilter(EventDataFilter):
205
232
  occurred: EventOccurredFilter = Field(
206
- default_factory=EventOccurredFilter,
233
+ default_factory=lambda: EventOccurredFilter(),
207
234
  description="Filter criteria for when the events occurred",
208
235
  )
209
236
  event: Optional[EventNameFilter] = Field(
@@ -220,7 +247,7 @@ class EventFilter(EventDataFilter):
220
247
  None, description="Filter criteria for the related resources of the event"
221
248
  )
222
249
  id: EventIDFilter = Field(
223
- default_factory=EventIDFilter,
250
+ default_factory=lambda: EventIDFilter(id=[]),
224
251
  description="Filter criteria for the events' ID",
225
252
  )
226
253
 
@@ -10,7 +10,9 @@ from typing import (
10
10
  Set,
11
11
  Tuple,
12
12
  Type,
13
+ TypeVar,
13
14
  Union,
15
+ cast,
14
16
  )
15
17
 
16
18
  from prefect.events import emit_event
@@ -41,45 +43,45 @@ def emit_instance_method_called_event(
41
43
  )
42
44
 
43
45
 
44
- def instrument_instance_method_call():
45
- def instrument(function):
46
- if is_instrumented(function):
47
- return function
46
+ F = TypeVar("F", bound=Callable)
48
47
 
49
- if inspect.iscoroutinefunction(function):
50
48
 
51
- @functools.wraps(function)
52
- async def inner(self, *args, **kwargs):
53
- success = True
54
- try:
55
- return await function(self, *args, **kwargs)
56
- except Exception as exc:
57
- success = False
58
- raise exc
59
- finally:
60
- emit_instance_method_called_event(
61
- instance=self, method_name=function.__name__, successful=success
62
- )
49
+ def instrument_instance_method_call(function: F) -> F:
50
+ if is_instrumented(function):
51
+ return function
63
52
 
64
- else:
53
+ if inspect.iscoroutinefunction(function):
65
54
 
66
- @functools.wraps(function)
67
- def inner(self, *args, **kwargs):
68
- success = True
69
- try:
70
- return function(self, *args, **kwargs)
71
- except Exception as exc:
72
- success = False
73
- raise exc
74
- finally:
75
- emit_instance_method_called_event(
76
- instance=self, method_name=function.__name__, successful=success
77
- )
55
+ @functools.wraps(function)
56
+ async def inner(self, *args, **kwargs):
57
+ success = True
58
+ try:
59
+ return await function(self, *args, **kwargs)
60
+ except Exception as exc:
61
+ success = False
62
+ raise exc
63
+ finally:
64
+ emit_instance_method_called_event(
65
+ instance=self, method_name=function.__name__, successful=success
66
+ )
78
67
 
79
- setattr(inner, "__events_instrumented__", True)
80
- return inner
68
+ else:
81
69
 
82
- return instrument
70
+ @functools.wraps(function)
71
+ def inner(self, *args, **kwargs):
72
+ success = True
73
+ try:
74
+ return function(self, *args, **kwargs)
75
+ except Exception as exc:
76
+ success = False
77
+ raise exc
78
+ finally:
79
+ emit_instance_method_called_event(
80
+ instance=self, method_name=function.__name__, successful=success
81
+ )
82
+
83
+ setattr(inner, "__events_instrumented__", True)
84
+ return cast(F, inner)
83
85
 
84
86
 
85
87
  def is_instrumented(function: Callable) -> bool:
@@ -119,17 +121,15 @@ def instrument_method_calls_on_class_instances(cls: Type) -> Type:
119
121
  """
120
122
 
121
123
  required_events_methods = ["_event_kind", "_event_method_called_resources"]
122
- for method in required_events_methods:
123
- if not hasattr(cls, method):
124
+ for method_name in required_events_methods:
125
+ if not hasattr(cls, method_name):
124
126
  raise RuntimeError(
125
- f"Unable to instrument class {cls}. Class must define {method!r}."
127
+ f"Unable to instrument class {cls}. Class must define {method_name!r}."
126
128
  )
127
129
 
128
- decorator = instrument_instance_method_call()
129
-
130
- for name, method in instrumentable_methods(
130
+ for method_name, method in instrumentable_methods(
131
131
  cls,
132
132
  exclude_methods=getattr(cls, "_events_excluded_methods", []),
133
133
  ):
134
- setattr(cls, name, decorator(method))
134
+ setattr(cls, method_name, instrument_instance_method_call(method))
135
135
  return cls
prefect/events/related.py CHANGED
@@ -57,6 +57,7 @@ async def related_resources_from_run_context(
57
57
  exclude: Optional[Set[str]] = None,
58
58
  ) -> List[RelatedResource]:
59
59
  from prefect.client.orchestration import get_client
60
+ from prefect.client.schemas.objects import FlowRun
60
61
  from prefect.context import FlowRunContext, TaskRunContext
61
62
 
62
63
  if exclude is None:
@@ -111,7 +112,7 @@ async def related_resources_from_run_context(
111
112
 
112
113
  flow_run = related_objects[0]["object"]
113
114
 
114
- if flow_run:
115
+ if isinstance(flow_run, FlowRun):
115
116
  related_objects += list(
116
117
  await asyncio.gather(
117
118
  _get_and_cache_related_object(