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.
- prefect/AGENTS.md +28 -0
- prefect/_build_info.py +3 -3
- prefect/_internal/websockets.py +109 -0
- prefect/artifacts.py +51 -2
- prefect/assets/core.py +2 -2
- prefect/blocks/core.py +82 -11
- prefect/client/cloud.py +11 -1
- prefect/client/orchestration/__init__.py +21 -15
- prefect/client/orchestration/_deployments/client.py +139 -4
- prefect/client/orchestration/_flows/client.py +4 -4
- prefect/client/schemas/__init__.py +5 -2
- prefect/client/schemas/actions.py +1 -0
- prefect/client/schemas/filters.py +3 -0
- prefect/client/schemas/objects.py +27 -10
- prefect/context.py +16 -7
- prefect/events/clients.py +2 -76
- prefect/events/schemas/automations.py +4 -0
- prefect/events/schemas/labelling.py +2 -0
- prefect/flow_engine.py +6 -3
- prefect/flows.py +64 -45
- prefect/futures.py +25 -4
- prefect/locking/filesystem.py +1 -1
- prefect/logging/clients.py +347 -0
- prefect/runner/runner.py +1 -1
- prefect/runner/submit.py +10 -4
- prefect/serializers.py +8 -3
- prefect/server/api/logs.py +64 -9
- prefect/server/api/server.py +2 -0
- prefect/server/api/templates.py +8 -2
- prefect/settings/context.py +17 -14
- prefect/settings/models/server/logs.py +28 -0
- prefect/settings/models/server/root.py +5 -0
- prefect/settings/models/server/services.py +26 -0
- prefect/task_engine.py +73 -43
- prefect/task_runners.py +10 -10
- prefect/tasks.py +52 -9
- prefect/types/__init__.py +2 -0
- prefect/types/names.py +50 -0
- prefect/utilities/_ast.py +2 -2
- prefect/utilities/callables.py +1 -1
- prefect/utilities/collections.py +6 -6
- prefect/utilities/engine.py +67 -72
- prefect/utilities/pydantic.py +19 -1
- prefect/workers/base.py +2 -0
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
- {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
|
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
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
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
|
-
|
1664
|
-
|
1665
|
-
|
1668
|
+
```python
|
1669
|
+
my_flow("marvin")
|
1670
|
+
```
|
1666
1671
|
|
1667
1672
|
Run a flow with additional tags
|
1668
1673
|
|
1669
|
-
|
1670
|
-
|
1671
|
-
|
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
|
-
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
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
|
-
|
1918
|
-
|
1919
|
-
|
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
|
-
|
1924
|
-
|
1925
|
-
|
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
|
-
|
1930
|
-
|
1931
|
-
|
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
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
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
|
-
|
298
|
-
|
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
|
|
prefect/locking/filesystem.py
CHANGED
@@ -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.
|
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
|
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[
|
70
|
-
|
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
|
-
|
72
|
-
|
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:
|