prefect-client 2.18.1__py3-none-any.whl → 2.18.2__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/automations.py +162 -0
- prefect/client/orchestration.py +29 -11
- prefect/client/schemas/objects.py +11 -8
- prefect/events/cli/automations.py +157 -34
- prefect/events/clients.py +3 -2
- prefect/events/filters.py +1 -1
- prefect/events/schemas/automations.py +2 -2
- prefect/events/schemas/deployment_triggers.py +1 -1
- prefect/events/schemas/events.py +11 -4
- prefect/events/schemas/labelling.py +1 -1
- prefect/flows.py +14 -11
- prefect/input/run_input.py +3 -1
- prefect/new_flow_engine.py +206 -55
- prefect/new_task_engine.py +159 -45
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +1 -1
- prefect/settings.py +21 -0
- prefect/tasks.py +134 -24
- prefect/utilities/asyncutils.py +16 -12
- prefect/workers/process.py +2 -1
- {prefect_client-2.18.1.dist-info → prefect_client-2.18.2.dist-info}/METADATA +1 -1
- {prefect_client-2.18.1.dist-info → prefect_client-2.18.2.dist-info}/RECORD +24 -23
- {prefect_client-2.18.1.dist-info → prefect_client-2.18.2.dist-info}/LICENSE +0 -0
- {prefect_client-2.18.1.dist-info → prefect_client-2.18.2.dist-info}/WHEEL +0 -0
- {prefect_client-2.18.1.dist-info → prefect_client-2.18.2.dist-info}/top_level.txt +0 -0
prefect/new_flow_engine.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import asyncio
|
2
|
-
|
2
|
+
import inspect
|
3
|
+
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
3
4
|
from dataclasses import dataclass
|
4
5
|
from typing import (
|
5
6
|
Any,
|
@@ -15,6 +16,8 @@ from typing import (
|
|
15
16
|
)
|
16
17
|
|
17
18
|
import anyio
|
19
|
+
import anyio._backends._asyncio
|
20
|
+
from sniffio import AsyncLibraryNotFoundError
|
18
21
|
from typing_extensions import ParamSpec
|
19
22
|
|
20
23
|
from prefect import Flow, Task, get_client
|
@@ -33,7 +36,8 @@ from prefect.states import (
|
|
33
36
|
exception_to_failed_state,
|
34
37
|
return_value_to_state,
|
35
38
|
)
|
36
|
-
from prefect.utilities.asyncutils import A, Async
|
39
|
+
from prefect.utilities.asyncutils import A, Async, run_sync
|
40
|
+
from prefect.utilities.callables import parameters_to_args_kwargs
|
37
41
|
from prefect.utilities.engine import (
|
38
42
|
_dynamic_key_for_task_run,
|
39
43
|
_resolve_custom_flow_run_name,
|
@@ -47,7 +51,7 @@ R = TypeVar("R")
|
|
47
51
|
|
48
52
|
@dataclass
|
49
53
|
class FlowRunEngine(Generic[P, R]):
|
50
|
-
flow: Flow[P, Coroutine[Any, Any, R]]
|
54
|
+
flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
|
51
55
|
parameters: Optional[Dict[str, Any]] = None
|
52
56
|
flow_run: Optional[FlowRun] = None
|
53
57
|
_is_started: bool = False
|
@@ -89,10 +93,17 @@ class FlowRunEngine(Generic[P, R]):
|
|
89
93
|
return state
|
90
94
|
|
91
95
|
async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
|
92
|
-
|
96
|
+
_result = self.state.result(raise_on_failure=raise_on_failure, fetch=True) # type: ignore
|
97
|
+
# state.result is a `sync_compatible` function that may or may not return an awaitable
|
98
|
+
# depending on whether the parent frame is sync or not
|
99
|
+
if inspect.isawaitable(_result):
|
100
|
+
_result = await _result
|
101
|
+
return _result
|
93
102
|
|
94
103
|
async def handle_success(self, result: R) -> R:
|
95
104
|
result_factory = getattr(FlowRunContext.get(), "result_factory", None)
|
105
|
+
if result_factory is None:
|
106
|
+
raise ValueError("Result factory is not set")
|
96
107
|
terminal_state = await return_value_to_state(
|
97
108
|
await resolve_futures_to_states(result),
|
98
109
|
result_factory=result_factory,
|
@@ -117,71 +128,102 @@ class FlowRunEngine(Generic[P, R]):
|
|
117
128
|
state = await self.set_state(Running())
|
118
129
|
return state
|
119
130
|
|
131
|
+
async def load_subflow_run(
|
132
|
+
self, parent_task_run: TaskRun, client: PrefectClient, context: FlowRunContext
|
133
|
+
) -> Union[FlowRun, None]:
|
134
|
+
"""
|
135
|
+
This method attempts to load an existing flow run for a subflow task
|
136
|
+
run, if appropriate.
|
137
|
+
|
138
|
+
If the parent task run is in a final but not COMPLETED state, and not
|
139
|
+
being rerun, then we attempt to load an existing flow run instead of
|
140
|
+
creating a new one. This will prevent the engine from running the
|
141
|
+
subflow again.
|
142
|
+
|
143
|
+
If no existing flow run is found, or if the subflow should be rerun,
|
144
|
+
then no flow run is returned.
|
145
|
+
"""
|
146
|
+
|
147
|
+
# check if the parent flow run is rerunning
|
148
|
+
rerunning = (
|
149
|
+
context.flow_run.run_count > 1
|
150
|
+
if getattr(context, "flow_run", None)
|
151
|
+
and isinstance(context.flow_run, FlowRun)
|
152
|
+
else False
|
153
|
+
)
|
154
|
+
|
155
|
+
# if the parent task run is in a final but not completed state, and
|
156
|
+
# not rerunning, then retrieve the most recent flow run instead of
|
157
|
+
# creating a new one. This effectively loads a cached flow run for
|
158
|
+
# situations where we are confident the flow should not be run
|
159
|
+
# again.
|
160
|
+
assert isinstance(parent_task_run.state, State)
|
161
|
+
if parent_task_run.state.is_final() and not (
|
162
|
+
rerunning and not parent_task_run.state.is_completed()
|
163
|
+
):
|
164
|
+
# return the most recent flow run, if it exists
|
165
|
+
flow_runs = await client.read_flow_runs(
|
166
|
+
flow_run_filter=FlowRunFilter(
|
167
|
+
parent_task_run_id={"any_": [parent_task_run.id]}
|
168
|
+
),
|
169
|
+
sort=FlowRunSort.EXPECTED_START_TIME_ASC,
|
170
|
+
limit=1,
|
171
|
+
)
|
172
|
+
if flow_runs:
|
173
|
+
return flow_runs[-1]
|
174
|
+
|
120
175
|
async def create_subflow_task_run(
|
121
176
|
self, client: PrefectClient, context: FlowRunContext
|
122
177
|
) -> TaskRun:
|
178
|
+
"""
|
179
|
+
Adds a task to a parent flow run that represents the execution of a subflow run.
|
180
|
+
|
181
|
+
The task run is referred to as the "parent task run" of the subflow and will be kept
|
182
|
+
in sync with the subflow run's state by the orchestration engine.
|
183
|
+
"""
|
123
184
|
dummy_task = Task(
|
124
185
|
name=self.flow.name, fn=self.flow.fn, version=self.flow.version
|
125
186
|
)
|
126
187
|
task_inputs = {
|
127
|
-
k: await collect_task_run_inputs(v)
|
188
|
+
k: await collect_task_run_inputs(v)
|
189
|
+
for k, v in (self.parameters or {}).items()
|
128
190
|
}
|
129
191
|
parent_task_run = await client.create_task_run(
|
130
192
|
task=dummy_task,
|
131
193
|
flow_run_id=(
|
132
|
-
context.flow_run.id
|
194
|
+
context.flow_run.id
|
195
|
+
if getattr(context, "flow_run", None)
|
196
|
+
and isinstance(context.flow_run, FlowRun)
|
197
|
+
else None
|
133
198
|
),
|
134
|
-
dynamic_key=_dynamic_key_for_task_run(context, dummy_task),
|
135
|
-
task_inputs=task_inputs,
|
199
|
+
dynamic_key=_dynamic_key_for_task_run(context, dummy_task), # type: ignore
|
200
|
+
task_inputs=task_inputs, # type: ignore
|
136
201
|
state=Pending(),
|
137
202
|
)
|
138
203
|
return parent_task_run
|
139
204
|
|
140
|
-
async def get_most_recent_flow_run_for_parent_task_run(
|
141
|
-
self, client: PrefectClient, parent_task_run: TaskRun
|
142
|
-
) -> "Union[FlowRun, None]":
|
143
|
-
"""
|
144
|
-
Get the most recent flow run associated with the provided parent task run.
|
145
|
-
|
146
|
-
Args:
|
147
|
-
- An orchestration client
|
148
|
-
- The parent task run to get the most recent flow run for
|
149
|
-
|
150
|
-
Returns:
|
151
|
-
The most recent flow run associated with the parent task run or `None` if
|
152
|
-
no flow runs are found
|
153
|
-
"""
|
154
|
-
flow_runs = await client.read_flow_runs(
|
155
|
-
flow_run_filter=FlowRunFilter(
|
156
|
-
parent_task_run_id={"any_": [parent_task_run.id]}
|
157
|
-
),
|
158
|
-
sort=FlowRunSort.EXPECTED_START_TIME_ASC,
|
159
|
-
)
|
160
|
-
return flow_runs[-1] if flow_runs else None
|
161
|
-
|
162
205
|
async def create_flow_run(self, client: PrefectClient) -> FlowRun:
|
163
206
|
flow_run_ctx = FlowRunContext.get()
|
207
|
+
parameters = self.parameters or {}
|
164
208
|
|
165
209
|
parent_task_run = None
|
210
|
+
|
166
211
|
# this is a subflow run
|
167
212
|
if flow_run_ctx:
|
213
|
+
# get the parent task run
|
168
214
|
parent_task_run = await self.create_subflow_task_run(
|
169
215
|
client=client, context=flow_run_ctx
|
170
216
|
)
|
171
|
-
|
172
|
-
#
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
)
|
178
|
-
)
|
179
|
-
if most_recent_flow_run:
|
180
|
-
return most_recent_flow_run
|
217
|
+
|
218
|
+
# check if there is already a flow run for this subflow
|
219
|
+
if subflow_run := await self.load_subflow_run(
|
220
|
+
parent_task_run=parent_task_run, client=client, context=flow_run_ctx
|
221
|
+
):
|
222
|
+
return subflow_run
|
181
223
|
|
182
224
|
try:
|
183
225
|
flow_run_name = _resolve_custom_flow_run_name(
|
184
|
-
flow=self.flow, parameters=
|
226
|
+
flow=self.flow, parameters=parameters
|
185
227
|
)
|
186
228
|
except TypeError:
|
187
229
|
flow_run_name = None
|
@@ -189,7 +231,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
189
231
|
flow_run = await client.create_flow_run(
|
190
232
|
flow=self.flow,
|
191
233
|
name=flow_run_name,
|
192
|
-
parameters=self.flow.serialize_parameters(
|
234
|
+
parameters=self.flow.serialize_parameters(parameters),
|
193
235
|
state=Pending(),
|
194
236
|
parent_task_run_id=getattr(parent_task_run, "id", None),
|
195
237
|
)
|
@@ -199,8 +241,46 @@ class FlowRunEngine(Generic[P, R]):
|
|
199
241
|
async def enter_run_context(self, client: Optional[PrefectClient] = None):
|
200
242
|
if client is None:
|
201
243
|
client = self.client
|
244
|
+
if not self.flow_run:
|
245
|
+
raise ValueError("Flow run not set")
|
202
246
|
|
203
247
|
self.flow_run = await client.read_flow_run(self.flow_run.id)
|
248
|
+
task_runner = self.flow.task_runner.duplicate()
|
249
|
+
|
250
|
+
async with AsyncExitStack() as stack:
|
251
|
+
task_runner = await stack.enter_async_context(
|
252
|
+
self.flow.task_runner.duplicate().start()
|
253
|
+
)
|
254
|
+
stack.enter_context(
|
255
|
+
FlowRunContext(
|
256
|
+
flow=self.flow,
|
257
|
+
log_prints=self.flow.log_prints or False,
|
258
|
+
flow_run=self.flow_run,
|
259
|
+
parameters=self.parameters,
|
260
|
+
client=client,
|
261
|
+
background_tasks=anyio.create_task_group(),
|
262
|
+
result_factory=await ResultFactory.from_flow(self.flow),
|
263
|
+
task_runner=task_runner,
|
264
|
+
)
|
265
|
+
)
|
266
|
+
self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
|
267
|
+
yield
|
268
|
+
|
269
|
+
@contextmanager
|
270
|
+
def enter_run_context_sync(self, client: Optional[PrefectClient] = None):
|
271
|
+
if client is None:
|
272
|
+
client = self.client
|
273
|
+
if not self.flow_run:
|
274
|
+
raise ValueError("Flow run not set")
|
275
|
+
|
276
|
+
self.flow_run = run_sync(client.read_flow_run(self.flow_run.id))
|
277
|
+
|
278
|
+
# if running in a completely synchronous frame, anyio will not detect the
|
279
|
+
# backend to use for the task group
|
280
|
+
try:
|
281
|
+
task_group = anyio.create_task_group()
|
282
|
+
except AsyncLibraryNotFoundError:
|
283
|
+
task_group = anyio._backends._asyncio.TaskGroup()
|
204
284
|
|
205
285
|
with FlowRunContext(
|
206
286
|
flow=self.flow,
|
@@ -208,8 +288,8 @@ class FlowRunEngine(Generic[P, R]):
|
|
208
288
|
flow_run=self.flow_run,
|
209
289
|
parameters=self.parameters,
|
210
290
|
client=client,
|
211
|
-
background_tasks=
|
212
|
-
result_factory=
|
291
|
+
background_tasks=task_group,
|
292
|
+
result_factory=run_sync(ResultFactory.from_flow(self.flow)),
|
213
293
|
task_runner=self.flow.task_runner,
|
214
294
|
):
|
215
295
|
self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
|
@@ -230,7 +310,9 @@ class FlowRunEngine(Generic[P, R]):
|
|
230
310
|
# validate prior to context so that context receives validated params
|
231
311
|
if self.flow.should_validate_parameters:
|
232
312
|
try:
|
233
|
-
self.parameters = self.flow.validate_parameters(
|
313
|
+
self.parameters = self.flow.validate_parameters(
|
314
|
+
self.parameters or {}
|
315
|
+
)
|
234
316
|
except Exception as exc:
|
235
317
|
await self.handle_exception(
|
236
318
|
exc,
|
@@ -238,11 +320,46 @@ class FlowRunEngine(Generic[P, R]):
|
|
238
320
|
result_factory=await ResultFactory.from_flow(self.flow),
|
239
321
|
)
|
240
322
|
self.short_circuit = True
|
323
|
+
try:
|
324
|
+
yield self
|
325
|
+
finally:
|
326
|
+
self._is_started = False
|
327
|
+
self._client = None
|
328
|
+
|
329
|
+
@contextmanager
|
330
|
+
def start_sync(self):
|
331
|
+
"""
|
332
|
+
Enters a client context and creates a flow run if needed.
|
333
|
+
"""
|
241
334
|
|
335
|
+
client = get_client()
|
336
|
+
run_sync(client.__aenter__())
|
337
|
+
self._client = client
|
338
|
+
self._is_started = True
|
339
|
+
|
340
|
+
if not self.flow_run:
|
341
|
+
self.flow_run = run_sync(self.create_flow_run(client))
|
342
|
+
|
343
|
+
# validate prior to context so that context receives validated params
|
344
|
+
if self.flow.should_validate_parameters:
|
345
|
+
try:
|
346
|
+
self.parameters = self.flow.validate_parameters(self.parameters or {})
|
347
|
+
except Exception as exc:
|
348
|
+
run_sync(
|
349
|
+
self.handle_exception(
|
350
|
+
exc,
|
351
|
+
msg="Validation of flow parameters failed with error",
|
352
|
+
result_factory=run_sync(ResultFactory.from_flow(self.flow)),
|
353
|
+
)
|
354
|
+
)
|
355
|
+
self.short_circuit = True
|
356
|
+
try:
|
242
357
|
yield self
|
243
|
-
|
244
|
-
|
245
|
-
|
358
|
+
finally:
|
359
|
+
# quickly close client
|
360
|
+
run_sync(client.__aexit__(None, None, None))
|
361
|
+
self._is_started = False
|
362
|
+
self._client = None
|
246
363
|
|
247
364
|
def is_running(self) -> bool:
|
248
365
|
if getattr(self, "flow_run", None) is None:
|
@@ -256,12 +373,12 @@ class FlowRunEngine(Generic[P, R]):
|
|
256
373
|
|
257
374
|
|
258
375
|
async def run_flow(
|
259
|
-
flow:
|
376
|
+
flow: Flow[P, Coroutine[Any, Any, R]],
|
260
377
|
flow_run: Optional[FlowRun] = None,
|
261
378
|
parameters: Optional[Dict[str, Any]] = None,
|
262
379
|
wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
|
263
380
|
return_type: Literal["state", "result"] = "result",
|
264
|
-
) ->
|
381
|
+
) -> Union[R, None]:
|
265
382
|
"""
|
266
383
|
Runs a flow against the API.
|
267
384
|
|
@@ -269,18 +386,19 @@ async def run_flow(
|
|
269
386
|
"""
|
270
387
|
|
271
388
|
engine = FlowRunEngine[P, R](flow, parameters, flow_run)
|
389
|
+
|
390
|
+
# This is a context manager that keeps track of the state of the flow run.
|
272
391
|
async with engine.start() as run:
|
273
|
-
# This is a context manager that keeps track of the state of the flow run.
|
274
392
|
await run.begin_run()
|
275
393
|
|
276
394
|
while run.is_running():
|
277
395
|
async with run.enter_run_context():
|
278
396
|
try:
|
279
397
|
# This is where the flow is actually run.
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
398
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
399
|
+
flow.fn, run.parameters or {}
|
400
|
+
)
|
401
|
+
result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
|
284
402
|
# If the flow run is successful, finalize it.
|
285
403
|
await run.handle_success(result)
|
286
404
|
|
@@ -291,3 +409,36 @@ async def run_flow(
|
|
291
409
|
if return_type == "state":
|
292
410
|
return run.state
|
293
411
|
return await run.result()
|
412
|
+
|
413
|
+
|
414
|
+
def run_flow_sync(
|
415
|
+
flow: Flow[P, R],
|
416
|
+
flow_run: Optional[FlowRun] = None,
|
417
|
+
parameters: Optional[Dict[str, Any]] = None,
|
418
|
+
wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
|
419
|
+
return_type: Literal["state", "result"] = "result",
|
420
|
+
) -> Union[R, State, None]:
|
421
|
+
engine = FlowRunEngine[P, R](flow, parameters, flow_run)
|
422
|
+
|
423
|
+
# This is a context manager that keeps track of the state of the flow run.
|
424
|
+
with engine.start_sync() as run:
|
425
|
+
run_sync(run.begin_run())
|
426
|
+
|
427
|
+
while run.is_running():
|
428
|
+
with run.enter_run_context_sync():
|
429
|
+
try:
|
430
|
+
# This is where the flow is actually run.
|
431
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
432
|
+
flow.fn, run.parameters or {}
|
433
|
+
)
|
434
|
+
result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
|
435
|
+
# If the flow run is successful, finalize it.
|
436
|
+
run_sync(run.handle_success(result))
|
437
|
+
|
438
|
+
except Exception as exc:
|
439
|
+
# If the flow fails, and we have retries left, set the flow to retrying.
|
440
|
+
run_sync(run.handle_exception(exc))
|
441
|
+
|
442
|
+
if return_type == "state":
|
443
|
+
return run.state
|
444
|
+
return run_sync(run.result())
|