prefect-client 3.4.6.dev1__py3-none-any.whl → 3.4.7__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 (48) hide show
  1. prefect/AGENTS.md +28 -0
  2. prefect/_build_info.py +3 -3
  3. prefect/_internal/websockets.py +109 -0
  4. prefect/artifacts.py +51 -2
  5. prefect/assets/core.py +2 -2
  6. prefect/blocks/core.py +82 -11
  7. prefect/client/cloud.py +11 -1
  8. prefect/client/orchestration/__init__.py +21 -15
  9. prefect/client/orchestration/_deployments/client.py +139 -4
  10. prefect/client/orchestration/_flows/client.py +4 -4
  11. prefect/client/schemas/__init__.py +5 -2
  12. prefect/client/schemas/actions.py +1 -0
  13. prefect/client/schemas/filters.py +3 -0
  14. prefect/client/schemas/objects.py +27 -10
  15. prefect/context.py +16 -7
  16. prefect/events/clients.py +2 -76
  17. prefect/events/schemas/automations.py +4 -0
  18. prefect/events/schemas/labelling.py +2 -0
  19. prefect/flow_engine.py +6 -3
  20. prefect/flows.py +64 -45
  21. prefect/futures.py +25 -4
  22. prefect/locking/filesystem.py +1 -1
  23. prefect/logging/clients.py +347 -0
  24. prefect/runner/runner.py +1 -1
  25. prefect/runner/submit.py +10 -4
  26. prefect/serializers.py +8 -3
  27. prefect/server/api/logs.py +64 -9
  28. prefect/server/api/server.py +2 -0
  29. prefect/server/api/templates.py +8 -2
  30. prefect/settings/context.py +17 -14
  31. prefect/settings/models/server/logs.py +28 -0
  32. prefect/settings/models/server/root.py +5 -0
  33. prefect/settings/models/server/services.py +26 -0
  34. prefect/task_engine.py +73 -43
  35. prefect/task_runners.py +10 -10
  36. prefect/tasks.py +52 -9
  37. prefect/types/__init__.py +2 -0
  38. prefect/types/names.py +50 -0
  39. prefect/utilities/_ast.py +2 -2
  40. prefect/utilities/callables.py +1 -1
  41. prefect/utilities/collections.py +6 -6
  42. prefect/utilities/engine.py +67 -72
  43. prefect/utilities/pydantic.py +19 -1
  44. prefect/workers/base.py +2 -0
  45. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
  46. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
  47. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
  48. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/licenses/LICENSE +0 -0
prefect/flows.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Module containing the base workflow class and decorator - for most use cases, using the [`@flow` decorator][prefect.flows.flow] is preferred.
2
+ Module containing the base workflow class and decorator - for most use cases, using the `@flow` decorator is preferred.
3
3
  """
4
4
 
5
5
  from __future__ import annotations
@@ -145,9 +145,6 @@ class Flow(Generic[P, R]):
145
145
  """
146
146
  A Prefect workflow definition.
147
147
 
148
- !!! note
149
- We recommend using the [`@flow` decorator][prefect.flows.flow] for most use-cases.
150
-
151
148
  Wraps a function with an entrypoint to the Prefect engine. To preserve the input
152
149
  and output types, we use the generic type variables `P` and `R` for "Parameters" and
153
150
  "Returns" respectively.
@@ -500,23 +497,29 @@ class Flow(Generic[P, R]):
500
497
 
501
498
  Create a new flow from an existing flow and update the name:
502
499
 
503
- >>> @flow(name="My flow")
504
- >>> def my_flow():
505
- >>> return 1
506
- >>>
507
- >>> new_flow = my_flow.with_options(name="My new flow")
500
+ ```python
501
+ from prefect import flow
502
+
503
+ @flow(name="My flow")
504
+ def my_flow():
505
+ return 1
506
+
507
+ new_flow = my_flow.with_options(name="My new flow")
508
+ ```
508
509
 
509
510
  Create a new flow from an existing flow, update the task runner, and call
510
511
  it without an intermediate variable:
511
512
 
512
- >>> from prefect.task_runners import ThreadPoolTaskRunner
513
- >>>
514
- >>> @flow
515
- >>> def my_flow(x, y):
516
- >>> return x + y
517
- >>>
518
- >>> state = my_flow.with_options(task_runner=ThreadPoolTaskRunner)(1, 3)
519
- >>> assert state.result() == 4
513
+ ```python
514
+ from prefect.task_runners import ThreadPoolTaskRunner
515
+
516
+ @flow
517
+ def my_flow(x, y):
518
+ return x + y
519
+
520
+ state = my_flow.with_options(task_runner=ThreadPoolTaskRunner)(1, 3)
521
+ assert state.result() == 4
522
+ ```
520
523
  """
521
524
  new_task_runner = (
522
525
  task_runner() if isinstance(task_runner, type) else task_runner
@@ -1653,22 +1656,27 @@ class Flow(Generic[P, R]):
1653
1656
 
1654
1657
  Define a flow
1655
1658
 
1656
- >>> @flow
1657
- >>> def my_flow(name):
1658
- >>> print(f"hello {name}")
1659
- >>> return f"goodbye {name}"
1659
+ ```python
1660
+ @flow
1661
+ def my_flow(name):
1662
+ print(f"hello {name}")
1663
+ return f"goodbye {name}"
1664
+ ```
1660
1665
 
1661
1666
  Run a flow
1662
1667
 
1663
- >>> my_flow("marvin")
1664
- hello marvin
1665
- "goodbye marvin"
1668
+ ```python
1669
+ my_flow("marvin")
1670
+ ```
1666
1671
 
1667
1672
  Run a flow with additional tags
1668
1673
 
1669
- >>> from prefect import tags
1670
- >>> with tags("db", "blue"):
1671
- >>> my_flow("foo")
1674
+ ```python
1675
+ from prefect import tags
1676
+
1677
+ with tags("db", "blue"):
1678
+ my_flow("foo")
1679
+ ```
1672
1680
  """
1673
1681
  from prefect.utilities.visualization import (
1674
1682
  get_task_viz_tracker,
@@ -1907,36 +1915,47 @@ class FlowDecorator:
1907
1915
  Examples:
1908
1916
  Define a simple flow
1909
1917
 
1910
- >>> from prefect import flow
1911
- >>> @flow
1912
- >>> def add(x, y):
1913
- >>> return x + y
1918
+ ```python
1919
+ from prefect import flow
1920
+
1921
+ @flow
1922
+ def add(x, y):
1923
+ return x + y
1924
+ ```
1914
1925
 
1915
1926
  Define an async flow
1916
1927
 
1917
- >>> @flow
1918
- >>> async def add(x, y):
1919
- >>> return x + y
1928
+ ```python
1929
+ @flow
1930
+ async def add(x, y):
1931
+ return x + y
1932
+ ```
1920
1933
 
1921
1934
  Define a flow with a version and description
1922
1935
 
1923
- >>> @flow(version="first-flow", description="This flow is empty!")
1924
- >>> def my_flow():
1925
- >>> pass
1936
+ ```python
1937
+ @flow(version="first-flow", description="This flow is empty!")
1938
+ def my_flow():
1939
+ pass
1940
+ ```
1926
1941
 
1927
1942
  Define a flow with a custom name
1928
1943
 
1929
- >>> @flow(name="The Ultimate Flow")
1930
- >>> def my_flow():
1931
- >>> pass
1944
+ ```python
1945
+ @flow(name="The Ultimate Flow")
1946
+ def my_flow():
1947
+ pass
1948
+ ```
1932
1949
 
1933
1950
  Define a flow that submits its tasks to dask
1934
1951
 
1935
- >>> from prefect_dask.task_runners import DaskTaskRunner
1936
- >>>
1937
- >>> @flow(task_runner=DaskTaskRunner)
1938
- >>> def my_flow():
1939
- >>> pass
1952
+ ```python
1953
+ from prefect_dask.task_runners import DaskTaskRunner
1954
+
1955
+ @flow(task_runner=DaskTaskRunner)
1956
+ def my_flow():
1957
+ pass
1958
+ ```
1940
1959
  """
1941
1960
  if __fn:
1942
1961
  return Flow(
prefect/futures.py CHANGED
@@ -272,9 +272,23 @@ class PrefectDistributedFuture(PrefectTaskRunFuture[R]):
272
272
  self.task_run_id,
273
273
  )
274
274
  await TaskRunWaiter.wait_for_task_run(self._task_run_id, timeout=timeout)
275
+
276
+ # After the waiter returns, we expect the task to be complete.
277
+ # However, there may be a small delay before the API reflects the final state
278
+ # due to eventual consistency between the event system and the API.
279
+ # We'll read the state and only cache it if it's final.
275
280
  task_run = await client.read_task_run(task_run_id=self._task_run_id)
276
- if task_run.state.is_final():
281
+ if task_run.state and task_run.state.is_final():
277
282
  self._final_state = task_run.state
283
+ else:
284
+ # Don't cache non-final states to avoid persisting stale data.
285
+ # result_async() will handle reading the state again if needed.
286
+ logger.debug(
287
+ "Task run %s state not yet final after wait (state: %s). "
288
+ "State will be re-read when needed.",
289
+ self.task_run_id,
290
+ task_run.state.type if task_run.state else "Unknown",
291
+ )
278
292
  return
279
293
 
280
294
  def result(
@@ -294,9 +308,16 @@ class PrefectDistributedFuture(PrefectTaskRunFuture[R]):
294
308
  if not self._final_state:
295
309
  await self.wait_async(timeout=timeout)
296
310
  if not self._final_state:
297
- raise TimeoutError(
298
- f"Task run {self.task_run_id} did not complete within {timeout} seconds"
299
- )
311
+ # If still no final state, try reading it directly as the
312
+ # state property does. This handles eventual consistency issues.
313
+ async with get_client() as client:
314
+ task_run = await client.read_task_run(task_run_id=self._task_run_id)
315
+ if task_run.state and task_run.state.is_final():
316
+ self._final_state = task_run.state
317
+ else:
318
+ raise TimeoutError(
319
+ f"Task run {self.task_run_id} did not complete within {timeout} seconds"
320
+ )
300
321
 
301
322
  return await self._final_state.aresult(raise_on_failure=raise_on_failure)
302
323
 
@@ -195,7 +195,7 @@ class FileSystemLockManager(LockManager):
195
195
  def release_lock(self, key: str, holder: str) -> None:
196
196
  lock_path = self._lock_path_for_key(key)
197
197
  if not self.is_locked(key):
198
- ValueError(f"No lock for transaction with key {key}")
198
+ raise ValueError(f"No lock for transaction with key {key}")
199
199
  if self.is_lock_holder(key, holder):
200
200
  Path(lock_path).unlink(missing_ok=True)
201
201
  self._locks.pop(key, None)
@@ -0,0 +1,347 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+ from types import TracebackType
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ MutableMapping,
8
+ Optional,
9
+ Tuple,
10
+ Type,
11
+ cast,
12
+ )
13
+ from uuid import UUID
14
+
15
+ import orjson
16
+ from cachetools import TTLCache
17
+ from prometheus_client import Counter
18
+ from typing_extensions import Self
19
+ from websockets import Subprotocol
20
+ from websockets.asyncio.client import ClientConnection
21
+ from websockets.exceptions import (
22
+ ConnectionClosed,
23
+ ConnectionClosedError,
24
+ ConnectionClosedOK,
25
+ )
26
+
27
+ from prefect._internal.websockets import (
28
+ create_ssl_context_for_websocket,
29
+ websocket_connect,
30
+ )
31
+ from prefect.client.schemas.objects import Log
32
+ from prefect.logging import get_logger
33
+ from prefect.settings import (
34
+ PREFECT_API_AUTH_STRING,
35
+ PREFECT_API_KEY,
36
+ PREFECT_API_URL,
37
+ PREFECT_CLOUD_API_URL,
38
+ PREFECT_SERVER_ALLOW_EPHEMERAL_MODE,
39
+ )
40
+ from prefect.types._datetime import now
41
+
42
+ if TYPE_CHECKING:
43
+ import logging
44
+
45
+ from prefect.client.schemas.filters import LogFilter
46
+
47
+ logger: "logging.Logger" = get_logger(__name__)
48
+
49
+ LOGS_OBSERVED = Counter(
50
+ "prefect_logs_observed",
51
+ "The number of logs observed by Prefect log subscribers",
52
+ labelnames=["client"],
53
+ )
54
+ LOG_WEBSOCKET_CONNECTIONS = Counter(
55
+ "prefect_log_websocket_connections",
56
+ (
57
+ "The number of times Prefect log clients have connected to a log stream, "
58
+ "broken down by direction (in/out) and connection (initial/reconnect)"
59
+ ),
60
+ labelnames=["client", "direction", "connection"],
61
+ )
62
+
63
+ SEEN_LOGS_SIZE = 500_000
64
+ SEEN_LOGS_TTL = 120
65
+
66
+
67
+ def http_to_ws(url: str) -> str:
68
+ return url.replace("https://", "wss://").replace("http://", "ws://").rstrip("/")
69
+
70
+
71
+ def logs_out_socket_from_api_url(url: str) -> str:
72
+ return http_to_ws(url) + "/logs/out"
73
+
74
+
75
+ def _get_api_url_and_key(
76
+ api_url: Optional[str], api_key: Optional[str]
77
+ ) -> Tuple[str, str]:
78
+ api_url = api_url or PREFECT_API_URL.value()
79
+ api_key = api_key or PREFECT_API_KEY.value()
80
+
81
+ if not api_url or not api_key:
82
+ raise ValueError(
83
+ "api_url and api_key must be provided or set in the Prefect configuration"
84
+ )
85
+
86
+ return api_url, api_key
87
+
88
+
89
+ def get_logs_subscriber(
90
+ filter: Optional["LogFilter"] = None,
91
+ reconnection_attempts: int = 10,
92
+ ) -> "PrefectLogsSubscriber":
93
+ """
94
+ Get a logs subscriber based on the current Prefect configuration.
95
+
96
+ Similar to get_events_subscriber, this automatically detects whether
97
+ you're using Prefect Cloud or OSS and returns the appropriate subscriber.
98
+ """
99
+ api_url = PREFECT_API_URL.value()
100
+
101
+ if isinstance(api_url, str) and api_url.startswith(PREFECT_CLOUD_API_URL.value()):
102
+ return PrefectCloudLogsSubscriber(
103
+ filter=filter, reconnection_attempts=reconnection_attempts
104
+ )
105
+ elif api_url:
106
+ return PrefectLogsSubscriber(
107
+ api_url=api_url,
108
+ filter=filter,
109
+ reconnection_attempts=reconnection_attempts,
110
+ )
111
+ elif PREFECT_SERVER_ALLOW_EPHEMERAL_MODE:
112
+ from prefect.server.api.server import SubprocessASGIServer
113
+
114
+ server = SubprocessASGIServer()
115
+ server.start()
116
+ return PrefectLogsSubscriber(
117
+ api_url=server.api_url,
118
+ filter=filter,
119
+ reconnection_attempts=reconnection_attempts,
120
+ )
121
+ else:
122
+ raise ValueError(
123
+ "No Prefect API URL provided. Please set PREFECT_API_URL to the address of a running Prefect server."
124
+ )
125
+
126
+
127
+ class PrefectLogsSubscriber:
128
+ """
129
+ Subscribes to a Prefect logs stream, yielding logs as they occur.
130
+
131
+ Example:
132
+
133
+ from prefect.logging.clients import PrefectLogsSubscriber
134
+ from prefect.client.schemas.filters import LogFilter, LogFilterLevel
135
+ import logging
136
+
137
+ filter = LogFilter(level=LogFilterLevel(ge_=logging.INFO))
138
+
139
+ async with PrefectLogsSubscriber(filter=filter) as subscriber:
140
+ async for log in subscriber:
141
+ print(log.timestamp, log.level, log.message)
142
+
143
+ """
144
+
145
+ _websocket: Optional[ClientConnection]
146
+ _filter: "LogFilter"
147
+ _seen_logs: MutableMapping[UUID, bool]
148
+
149
+ _api_key: Optional[str]
150
+ _auth_token: Optional[str]
151
+
152
+ def __init__(
153
+ self,
154
+ api_url: Optional[str] = None,
155
+ filter: Optional["LogFilter"] = None,
156
+ reconnection_attempts: int = 10,
157
+ ):
158
+ """
159
+ Args:
160
+ api_url: The base URL for a Prefect workspace
161
+ filter: Log filter to apply
162
+ reconnection_attempts: When the client is disconnected, how many times
163
+ the client should attempt to reconnect
164
+ """
165
+ self._api_key = None
166
+ self._auth_token = PREFECT_API_AUTH_STRING.value()
167
+
168
+ if not api_url:
169
+ api_url = cast(str, PREFECT_API_URL.value())
170
+
171
+ from prefect.client.schemas.filters import LogFilter
172
+
173
+ self._filter = filter or LogFilter() # type: ignore[call-arg]
174
+ self._seen_logs = TTLCache(maxsize=SEEN_LOGS_SIZE, ttl=SEEN_LOGS_TTL)
175
+
176
+ socket_url = logs_out_socket_from_api_url(api_url)
177
+
178
+ logger.debug("Connecting to %s", socket_url)
179
+
180
+ # Configure SSL context for the connection
181
+ ssl_context = create_ssl_context_for_websocket(socket_url)
182
+ connect_kwargs: dict[str, Any] = {"subprotocols": [Subprotocol("prefect")]}
183
+ if ssl_context:
184
+ connect_kwargs["ssl"] = ssl_context
185
+
186
+ self._connect = websocket_connect(socket_url, **connect_kwargs)
187
+ self._websocket = None
188
+ self._reconnection_attempts = reconnection_attempts
189
+ if self._reconnection_attempts < 0:
190
+ raise ValueError("reconnection_attempts must be a non-negative integer")
191
+
192
+ @property
193
+ def client_name(self) -> str:
194
+ return self.__class__.__name__
195
+
196
+ async def __aenter__(self) -> Self:
197
+ # Don't handle any errors in the initial connection, because these are most
198
+ # likely a permission or configuration issue that should propagate
199
+ try:
200
+ await self._reconnect()
201
+ finally:
202
+ LOG_WEBSOCKET_CONNECTIONS.labels(self.client_name, "out", "initial").inc()
203
+ return self
204
+
205
+ async def _reconnect(self) -> None:
206
+ logger.debug("Reconnecting...")
207
+ if self._websocket:
208
+ self._websocket = None
209
+ await self._connect.__aexit__(None, None, None)
210
+
211
+ self._websocket = await self._connect.__aenter__()
212
+
213
+ # make sure we have actually connected
214
+ logger.debug(" pinging...")
215
+ pong = await self._websocket.ping()
216
+ await pong
217
+
218
+ # Send authentication message - logs WebSocket requires auth handshake
219
+ auth_token = self._api_key or self._auth_token
220
+ auth_message = {"type": "auth", "token": auth_token}
221
+ logger.debug(" authenticating...")
222
+ await self._websocket.send(orjson.dumps(auth_message).decode())
223
+
224
+ # Wait for auth response
225
+ try:
226
+ message = orjson.loads(await self._websocket.recv())
227
+ logger.debug(" auth result %s", message)
228
+ assert message["type"] == "auth_success", message.get("reason", "")
229
+ except AssertionError as e:
230
+ raise Exception(
231
+ "Unable to authenticate to the log stream. Please ensure the "
232
+ "provided api_key or auth_token you are using is valid for this environment. "
233
+ f"Reason: {e.args[0]}"
234
+ )
235
+ except ConnectionClosedError as e:
236
+ reason = getattr(e.rcvd, "reason", None)
237
+ msg = "Unable to authenticate to the log stream. Please ensure the "
238
+ msg += "provided api_key or auth_token you are using is valid for this environment. "
239
+ msg += f"Reason: {reason}" if reason else ""
240
+ raise Exception(msg) from e
241
+
242
+ from prefect.client.schemas.filters import LogFilterTimestamp
243
+
244
+ current_time = now("UTC")
245
+ self._filter.timestamp = LogFilterTimestamp(
246
+ after_=current_time - timedelta(minutes=1), # type: ignore[arg-type]
247
+ before_=current_time + timedelta(days=365), # type: ignore[arg-type]
248
+ )
249
+
250
+ logger.debug(" filtering logs since %s...", self._filter.timestamp.after_)
251
+ filter_message = {
252
+ "type": "filter",
253
+ "filter": self._filter.model_dump(mode="json"),
254
+ }
255
+ await self._websocket.send(orjson.dumps(filter_message).decode())
256
+
257
+ async def __aexit__(
258
+ self,
259
+ exc_type: Optional[Type[BaseException]],
260
+ exc_val: Optional[BaseException],
261
+ exc_tb: Optional[TracebackType],
262
+ ) -> None:
263
+ self._websocket = None
264
+ await self._connect.__aexit__(exc_type, exc_val, exc_tb)
265
+
266
+ def __aiter__(self) -> Self:
267
+ return self
268
+
269
+ async def __anext__(self) -> Log:
270
+ assert self._reconnection_attempts >= 0
271
+ for i in range(self._reconnection_attempts + 1): # pragma: no branch
272
+ try:
273
+ # If we're here and the websocket is None, then we've had a failure in a
274
+ # previous reconnection attempt.
275
+ #
276
+ # Otherwise, after the first time through this loop, we're recovering
277
+ # from a ConnectionClosed, so reconnect now.
278
+ if not self._websocket or i > 0:
279
+ try:
280
+ await self._reconnect()
281
+ finally:
282
+ LOG_WEBSOCKET_CONNECTIONS.labels(
283
+ self.client_name, "out", "reconnect"
284
+ ).inc()
285
+ assert self._websocket
286
+
287
+ while True:
288
+ message = orjson.loads(await self._websocket.recv())
289
+ log: Log = Log.model_validate(message["log"])
290
+
291
+ if log.id in self._seen_logs:
292
+ continue
293
+ self._seen_logs[log.id] = True
294
+
295
+ try:
296
+ return log
297
+ finally:
298
+ LOGS_OBSERVED.labels(self.client_name).inc()
299
+
300
+ except ConnectionClosedOK:
301
+ logger.debug('Connection closed with "OK" status')
302
+ raise StopAsyncIteration
303
+ except ConnectionClosed:
304
+ logger.debug(
305
+ "Connection closed with %s/%s attempts",
306
+ i + 1,
307
+ self._reconnection_attempts,
308
+ )
309
+ if i == self._reconnection_attempts:
310
+ # this was our final chance, raise the most recent error
311
+ raise
312
+
313
+ if i > 2:
314
+ # let the first two attempts happen quickly in case this is just
315
+ # a standard load balancer timeout, but after that, just take a
316
+ # beat to let things come back around.
317
+ await asyncio.sleep(1)
318
+ raise StopAsyncIteration
319
+
320
+
321
+ class PrefectCloudLogsSubscriber(PrefectLogsSubscriber):
322
+ """Logs subscriber for Prefect Cloud"""
323
+
324
+ def __init__(
325
+ self,
326
+ api_url: Optional[str] = None,
327
+ api_key: Optional[str] = None,
328
+ filter: Optional["LogFilter"] = None,
329
+ reconnection_attempts: int = 10,
330
+ ):
331
+ """
332
+ Args:
333
+ api_url: The base URL for a Prefect Cloud workspace
334
+ api_key: The API key of an actor with the see_flows scope
335
+ filter: Log filter to apply
336
+ reconnection_attempts: When the client is disconnected, how many times
337
+ the client should attempt to reconnect
338
+ """
339
+ api_url, api_key = _get_api_url_and_key(api_url, api_key)
340
+
341
+ super().__init__(
342
+ api_url=api_url,
343
+ filter=filter,
344
+ reconnection_attempts=reconnection_attempts,
345
+ )
346
+
347
+ self._api_key = api_key
prefect/runner/runner.py CHANGED
@@ -945,7 +945,7 @@ class Runner:
945
945
  """
946
946
  self._logger.info("Pausing all deployments...")
947
947
  for deployment_id in self._deployment_ids:
948
- await self._client.set_deployment_paused_state(deployment_id, True)
948
+ await self._client.pause_deployment(deployment_id)
949
949
  self._logger.debug(f"Paused deployment '{deployment_id}'")
950
950
 
951
951
  self._logger.info("All deployments have been paused!")
prefect/runner/submit.py CHANGED
@@ -17,7 +17,13 @@ from prefect.client.schemas.filters import (
17
17
  FlowRunFilterParentFlowRunId,
18
18
  TaskRunFilter,
19
19
  )
20
- from prefect.client.schemas.objects import Constant, FlowRun, Parameter, TaskRunResult
20
+ from prefect.client.schemas.objects import (
21
+ Constant,
22
+ FlowRun,
23
+ FlowRunResult,
24
+ Parameter,
25
+ TaskRunResult,
26
+ )
21
27
  from prefect.context import FlowRunContext
22
28
  from prefect.flows import Flow
23
29
  from prefect.logging import get_logger
@@ -66,9 +72,9 @@ async def _submit_flow_to_runner(
66
72
 
67
73
  parent_flow_run_context = FlowRunContext.get()
68
74
 
69
- task_inputs: dict[str, list[TaskRunResult | Parameter | Constant]] = {
70
- k: list(await collect_task_run_inputs(v)) for k, v in parameters.items()
71
- }
75
+ task_inputs: dict[
76
+ str, list[Union[TaskRunResult, FlowRunResult, Parameter, Constant]]
77
+ ] = {k: list(await collect_task_run_inputs(v)) for k, v in parameters.items()}
72
78
  parameters = await resolve_inputs(parameters)
73
79
  dummy_task = Task(name=flow.name, fn=flow.fn, version=flow.version)
74
80
  parent_task_run = await client.create_task_run(
prefect/serializers.py CHANGED
@@ -38,6 +38,8 @@ from prefect.utilities.pydantic import custom_pydantic_encoder
38
38
 
39
39
  D = TypeVar("D", default=Any)
40
40
 
41
+ _TYPE_ADAPTER_CACHE: dict[str, TypeAdapter[Any]] = {}
42
+
41
43
 
42
44
  def prefect_json_object_encoder(obj: Any) -> Any:
43
45
  """
@@ -68,9 +70,12 @@ def prefect_json_object_decoder(result: dict[str, Any]) -> Any:
68
70
  with `prefect_json_object_encoder`
69
71
  """
70
72
  if "__class__" in result:
71
- return TypeAdapter(from_qualified_name(result["__class__"])).validate_python(
72
- result["data"]
73
- )
73
+ class_name = result["__class__"]
74
+ if class_name not in _TYPE_ADAPTER_CACHE:
75
+ _TYPE_ADAPTER_CACHE[class_name] = TypeAdapter(
76
+ from_qualified_name(class_name)
77
+ )
78
+ return _TYPE_ADAPTER_CACHE[class_name].validate_python(result["data"])
74
79
  elif "__exc_type__" in result:
75
80
  return from_qualified_name(result["__exc_type__"])(result["message"])
76
81
  else: