prefect-client 2.18.2__py3-none-any.whl → 2.19.0__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 (41) hide show
  1. prefect/__init__.py +1 -15
  2. prefect/_internal/concurrency/cancellation.py +2 -0
  3. prefect/_internal/schemas/validators.py +10 -0
  4. prefect/_vendor/starlette/testclient.py +1 -1
  5. prefect/blocks/notifications.py +6 -6
  6. prefect/client/base.py +244 -1
  7. prefect/client/cloud.py +4 -2
  8. prefect/client/orchestration.py +515 -106
  9. prefect/client/schemas/actions.py +58 -8
  10. prefect/client/schemas/objects.py +15 -1
  11. prefect/client/schemas/responses.py +19 -0
  12. prefect/client/schemas/schedules.py +1 -1
  13. prefect/client/utilities.py +2 -2
  14. prefect/concurrency/asyncio.py +34 -4
  15. prefect/concurrency/sync.py +40 -6
  16. prefect/context.py +2 -2
  17. prefect/engine.py +17 -1
  18. prefect/events/clients.py +2 -2
  19. prefect/flows.py +91 -17
  20. prefect/infrastructure/process.py +0 -17
  21. prefect/logging/formatters.py +1 -4
  22. prefect/new_flow_engine.py +166 -161
  23. prefect/new_task_engine.py +137 -202
  24. prefect/runner/__init__.py +1 -1
  25. prefect/runner/runner.py +2 -107
  26. prefect/settings.py +11 -0
  27. prefect/tasks.py +76 -57
  28. prefect/types/__init__.py +27 -5
  29. prefect/utilities/annotations.py +1 -8
  30. prefect/utilities/asyncutils.py +4 -0
  31. prefect/utilities/engine.py +106 -1
  32. prefect/utilities/schema_tools/__init__.py +6 -1
  33. prefect/utilities/schema_tools/validation.py +25 -8
  34. prefect/utilities/timeout.py +34 -0
  35. prefect/workers/base.py +7 -3
  36. prefect/workers/process.py +0 -17
  37. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/METADATA +1 -1
  38. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/RECORD +41 -40
  39. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/LICENSE +0 -0
  40. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/WHEEL +0 -0
  41. {prefect_client-2.18.2.dist-info → prefect_client-2.19.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
- import asyncio
2
1
  import inspect
3
- from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
4
- from dataclasses import dataclass
2
+ import logging
3
+ import os
4
+ import time
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass, field
5
7
  from typing import (
6
8
  Any,
7
9
  Coroutine,
@@ -10,60 +12,84 @@ from typing import (
10
12
  Iterable,
11
13
  Literal,
12
14
  Optional,
15
+ Tuple,
13
16
  TypeVar,
14
17
  Union,
15
18
  cast,
16
19
  )
20
+ from uuid import UUID
17
21
 
18
22
  import anyio
19
23
  import anyio._backends._asyncio
20
24
  from sniffio import AsyncLibraryNotFoundError
21
25
  from typing_extensions import ParamSpec
22
26
 
23
- from prefect import Flow, Task, get_client
24
- from prefect.client.orchestration import PrefectClient
27
+ from prefect import Task, get_client
28
+ from prefect.client.orchestration import SyncPrefectClient
25
29
  from prefect.client.schemas import FlowRun, TaskRun
26
30
  from prefect.client.schemas.filters import FlowRunFilter
27
31
  from prefect.client.schemas.sorting import FlowRunSort
28
32
  from prefect.context import FlowRunContext
33
+ from prefect.deployments import load_flow_from_flow_run
34
+ from prefect.exceptions import Abort, Pause
35
+ from prefect.flows import Flow, load_flow_from_entrypoint
29
36
  from prefect.futures import PrefectFuture, resolve_futures_to_states
30
- from prefect.logging.loggers import flow_run_logger
37
+ from prefect.logging.loggers import flow_run_logger, get_logger
31
38
  from prefect.results import ResultFactory
32
39
  from prefect.states import (
33
40
  Pending,
34
41
  Running,
35
42
  State,
43
+ exception_to_crashed_state,
36
44
  exception_to_failed_state,
37
45
  return_value_to_state,
38
46
  )
39
47
  from prefect.utilities.asyncutils import A, Async, run_sync
40
48
  from prefect.utilities.callables import parameters_to_args_kwargs
41
49
  from prefect.utilities.engine import (
42
- _dynamic_key_for_task_run,
43
50
  _resolve_custom_flow_run_name,
44
- collect_task_run_inputs,
45
- propose_state,
51
+ propose_state_sync,
46
52
  )
47
53
 
48
54
  P = ParamSpec("P")
49
55
  R = TypeVar("R")
50
56
 
51
57
 
58
+ def load_flow_and_flow_run(flow_run_id: UUID) -> Tuple[FlowRun, Flow]:
59
+ ## TODO: add error handling to update state and log tracebacks
60
+ entrypoint = os.environ.get("PREFECT__FLOW_ENTRYPOINT")
61
+
62
+ client = get_client(sync_client=True)
63
+ flow_run = client.read_flow_run(flow_run_id)
64
+ flow = (
65
+ load_flow_from_entrypoint(entrypoint)
66
+ if entrypoint
67
+ else run_sync(load_flow_from_flow_run(flow_run, client=client))
68
+ )
69
+
70
+ return flow_run, flow
71
+
72
+
52
73
  @dataclass
53
74
  class FlowRunEngine(Generic[P, R]):
54
- flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
75
+ flow: Optional[Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]] = None
55
76
  parameters: Optional[Dict[str, Any]] = None
56
77
  flow_run: Optional[FlowRun] = None
78
+ flow_run_id: Optional[UUID] = None
79
+ logger: logging.Logger = field(default_factory=lambda: get_logger("engine"))
57
80
  _is_started: bool = False
58
- _client: Optional[PrefectClient] = None
81
+ _client: Optional[SyncPrefectClient] = None
59
82
  short_circuit: bool = False
60
83
 
61
84
  def __post_init__(self):
85
+ if self.flow is None and self.flow_run_id is None:
86
+ raise ValueError("Either a flow or a flow_run_id must be provided.")
87
+
62
88
  if self.parameters is None:
63
89
  self.parameters = {}
64
90
 
65
91
  @property
66
- def client(self) -> PrefectClient:
92
+ def client(self) -> SyncPrefectClient:
67
93
  if not self._is_started or self._client is None:
68
94
  raise RuntimeError("Engine has not started.")
69
95
  return self._client
@@ -72,64 +98,80 @@ class FlowRunEngine(Generic[P, R]):
72
98
  def state(self) -> State:
73
99
  return self.flow_run.state # type: ignore
74
100
 
75
- async def begin_run(self) -> State:
101
+ def begin_run(self) -> State:
76
102
  new_state = Running()
77
- state = await self.set_state(new_state)
103
+ state = self.set_state(new_state)
78
104
  while state.is_pending():
79
- await asyncio.sleep(1)
80
- state = await self.set_state(new_state)
105
+ time.sleep(0.2)
106
+ state = self.set_state(new_state)
81
107
  return state
82
108
 
83
- async def set_state(self, state: State) -> State:
109
+ def set_state(self, state: State, force: bool = False) -> State:
84
110
  """ """
85
111
  # prevents any state-setting activity
86
112
  if self.short_circuit:
87
113
  return self.state
88
114
 
89
- state = await propose_state(self.client, state, flow_run_id=self.flow_run.id) # type: ignore
115
+ state = propose_state_sync(
116
+ self.client, state, flow_run_id=self.flow_run.id, force=force
117
+ ) # type: ignore
90
118
  self.flow_run.state = state # type: ignore
91
119
  self.flow_run.state_name = state.name # type: ignore
92
120
  self.flow_run.state_type = state.type # type: ignore
93
121
  return state
94
122
 
95
- async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
123
+ def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
96
124
  _result = self.state.result(raise_on_failure=raise_on_failure, fetch=True) # type: ignore
97
125
  # state.result is a `sync_compatible` function that may or may not return an awaitable
98
126
  # depending on whether the parent frame is sync or not
99
127
  if inspect.isawaitable(_result):
100
- _result = await _result
128
+ _result = run_sync(_result)
101
129
  return _result
102
130
 
103
- async def handle_success(self, result: R) -> R:
131
+ def handle_success(self, result: R) -> R:
104
132
  result_factory = getattr(FlowRunContext.get(), "result_factory", None)
105
133
  if result_factory is None:
106
134
  raise ValueError("Result factory is not set")
107
- terminal_state = await return_value_to_state(
108
- await resolve_futures_to_states(result),
109
- result_factory=result_factory,
135
+ terminal_state = run_sync(
136
+ return_value_to_state(
137
+ run_sync(resolve_futures_to_states(result)),
138
+ result_factory=result_factory,
139
+ )
110
140
  )
111
- await self.set_state(terminal_state)
141
+ self.set_state(terminal_state)
112
142
  return result
113
143
 
114
- async def handle_exception(
144
+ def handle_exception(
115
145
  self,
116
146
  exc: Exception,
117
147
  msg: Optional[str] = None,
118
148
  result_factory: Optional[ResultFactory] = None,
119
149
  ) -> State:
120
150
  context = FlowRunContext.get()
121
- state = await exception_to_failed_state(
122
- exc,
123
- message=msg or "Flow run encountered an exception:",
124
- result_factory=result_factory or getattr(context, "result_factory", None),
151
+ state = run_sync(
152
+ exception_to_failed_state(
153
+ exc,
154
+ message=msg or "Flow run encountered an exception:",
155
+ result_factory=result_factory
156
+ or getattr(context, "result_factory", None),
157
+ )
125
158
  )
126
- state = await self.set_state(state)
159
+ state = self.set_state(state)
127
160
  if self.state.is_scheduled():
128
- state = await self.set_state(Running())
161
+ state = self.set_state(Running())
129
162
  return state
130
163
 
131
- async def load_subflow_run(
132
- self, parent_task_run: TaskRun, client: PrefectClient, context: FlowRunContext
164
+ def handle_crash(self, exc: BaseException) -> None:
165
+ state = run_sync(exception_to_crashed_state(exc))
166
+ self.logger.error(f"Crash detected! {state.message}")
167
+ self.logger.debug("Crash details:", exc_info=exc)
168
+ self.set_state(state, force=True)
169
+
170
+ def load_subflow_run(
171
+ self,
172
+ parent_task_run: TaskRun,
173
+ client: SyncPrefectClient,
174
+ context: FlowRunContext,
133
175
  ) -> Union[FlowRun, None]:
134
176
  """
135
177
  This method attempts to load an existing flow run for a subflow task
@@ -162,7 +204,7 @@ class FlowRunEngine(Generic[P, R]):
162
204
  rerunning and not parent_task_run.state.is_completed()
163
205
  ):
164
206
  # return the most recent flow run, if it exists
165
- flow_runs = await client.read_flow_runs(
207
+ flow_runs = client.read_flow_runs(
166
208
  flow_run_filter=FlowRunFilter(
167
209
  parent_task_run_id={"any_": [parent_task_run.id]}
168
210
  ),
@@ -172,37 +214,7 @@ class FlowRunEngine(Generic[P, R]):
172
214
  if flow_runs:
173
215
  return flow_runs[-1]
174
216
 
175
- async def create_subflow_task_run(
176
- self, client: PrefectClient, context: FlowRunContext
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
- """
184
- dummy_task = Task(
185
- name=self.flow.name, fn=self.flow.fn, version=self.flow.version
186
- )
187
- task_inputs = {
188
- k: await collect_task_run_inputs(v)
189
- for k, v in (self.parameters or {}).items()
190
- }
191
- parent_task_run = await client.create_task_run(
192
- task=dummy_task,
193
- 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
198
- ),
199
- dynamic_key=_dynamic_key_for_task_run(context, dummy_task), # type: ignore
200
- task_inputs=task_inputs, # type: ignore
201
- state=Pending(),
202
- )
203
- return parent_task_run
204
-
205
- async def create_flow_run(self, client: PrefectClient) -> FlowRun:
217
+ def create_flow_run(self, client: SyncPrefectClient) -> FlowRun:
206
218
  flow_run_ctx = FlowRunContext.get()
207
219
  parameters = self.parameters or {}
208
220
 
@@ -210,13 +222,21 @@ class FlowRunEngine(Generic[P, R]):
210
222
 
211
223
  # this is a subflow run
212
224
  if flow_run_ctx:
213
- # get the parent task run
214
- parent_task_run = await self.create_subflow_task_run(
215
- client=client, context=flow_run_ctx
225
+ # add a task to a parent flow run that represents the execution of a subflow run
226
+ # reuse the logic from the TaskRunEngine to ensure parents are created correctly
227
+ parent_task = Task(
228
+ name=self.flow.name, fn=self.flow.fn, version=self.flow.version
229
+ )
230
+ parent_task_run = run_sync(
231
+ parent_task.create_run(
232
+ client=self.client,
233
+ flow_run_context=flow_run_ctx,
234
+ parameters=self.parameters,
235
+ )
216
236
  )
217
237
 
218
238
  # check if there is already a flow run for this subflow
219
- if subflow_run := await self.load_subflow_run(
239
+ if subflow_run := self.load_subflow_run(
220
240
  parent_task_run=parent_task_run, client=client, context=flow_run_ctx
221
241
  ):
222
242
  return subflow_run
@@ -228,7 +248,7 @@ class FlowRunEngine(Generic[P, R]):
228
248
  except TypeError:
229
249
  flow_run_name = None
230
250
 
231
- flow_run = await client.create_flow_run(
251
+ flow_run = client.create_flow_run(
232
252
  flow=self.flow,
233
253
  name=flow_run_name,
234
254
  parameters=self.flow.serialize_parameters(parameters),
@@ -237,43 +257,14 @@ class FlowRunEngine(Generic[P, R]):
237
257
  )
238
258
  return flow_run
239
259
 
240
- @asynccontextmanager
241
- async def enter_run_context(self, client: Optional[PrefectClient] = None):
242
- if client is None:
243
- client = self.client
244
- if not self.flow_run:
245
- raise ValueError("Flow run not set")
246
-
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
260
  @contextmanager
270
- def enter_run_context_sync(self, client: Optional[PrefectClient] = None):
261
+ def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
271
262
  if client is None:
272
263
  client = self.client
273
264
  if not self.flow_run:
274
265
  raise ValueError("Flow run not set")
275
266
 
276
- self.flow_run = run_sync(client.read_flow_run(self.flow_run.id))
267
+ self.flow_run = client.read_flow_run(self.flow_run.id)
277
268
 
278
269
  # if running in a completely synchronous frame, anyio will not detect the
279
270
  # backend to use for the task group
@@ -290,22 +281,41 @@ class FlowRunEngine(Generic[P, R]):
290
281
  client=client,
291
282
  background_tasks=task_group,
292
283
  result_factory=run_sync(ResultFactory.from_flow(self.flow)),
293
- task_runner=self.flow.task_runner,
284
+ task_runner=self.flow.task_runner.duplicate(),
294
285
  ):
295
- self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
296
- yield
286
+ # set the logger to the flow run logger
287
+ current_logger = self.logger
288
+ try:
289
+ self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
290
+ yield
291
+ finally:
292
+ self.logger = current_logger
297
293
 
298
- @asynccontextmanager
299
- async def start(self):
294
+ @contextmanager
295
+ def start(self):
300
296
  """
301
297
  Enters a client context and creates a flow run if needed.
302
298
  """
303
- async with get_client() as client:
299
+
300
+ with get_client(sync_client=True) as client:
304
301
  self._client = client
305
302
  self._is_started = True
306
303
 
304
+ # this conditional is engaged whenever a run is triggered via deployment
305
+ if self.flow_run_id and not self.flow:
306
+ self.flow_run = client.read_flow_run(self.flow_run_id)
307
+ try:
308
+ self.flow = self.load_flow(client)
309
+ except Exception as exc:
310
+ self.handle_exception(
311
+ exc,
312
+ msg="Failed to load flow from entrypoint.",
313
+ )
314
+ self.short_circuit = True
315
+
307
316
  if not self.flow_run:
308
- self.flow_run = await self.create_flow_run(client)
317
+ self.flow_run = self.create_flow_run(client)
318
+ self.logger.debug(f'Created flow run "{self.flow_run.id}"')
309
319
 
310
320
  # validate prior to context so that context receives validated params
311
321
  if self.flow.should_validate_parameters:
@@ -314,53 +324,27 @@ class FlowRunEngine(Generic[P, R]):
314
324
  self.parameters or {}
315
325
  )
316
326
  except Exception as exc:
317
- await self.handle_exception(
327
+ self.handle_exception(
318
328
  exc,
319
329
  msg="Validation of flow parameters failed with error",
320
- result_factory=await ResultFactory.from_flow(self.flow),
330
+ result_factory=run_sync(ResultFactory.from_flow(self.flow)),
321
331
  )
322
332
  self.short_circuit = True
323
333
  try:
324
334
  yield self
335
+ except Exception:
336
+ # regular exceptions are caught and re-raised to the user
337
+ raise
338
+ except (Abort, Pause):
339
+ raise
340
+ except BaseException as exc:
341
+ # BaseExceptions are caught and handled as crashes
342
+ self.handle_crash(exc)
343
+ raise
325
344
  finally:
326
345
  self._is_started = False
327
346
  self._client = None
328
347
 
329
- @contextmanager
330
- def start_sync(self):
331
- """
332
- Enters a client context and creates a flow run if needed.
333
- """
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:
357
- yield self
358
- finally:
359
- # quickly close client
360
- run_sync(client.__aexit__(None, None, None))
361
- self._is_started = False
362
- self._client = None
363
-
364
348
  def is_running(self) -> bool:
365
349
  if getattr(self, "flow_run", None) is None:
366
350
  return False
@@ -372,9 +356,10 @@ class FlowRunEngine(Generic[P, R]):
372
356
  return getattr(self, "flow_run").state.is_pending()
373
357
 
374
358
 
375
- async def run_flow(
376
- flow: Flow[P, Coroutine[Any, Any, R]],
359
+ async def run_flow_async(
360
+ flow: Optional[Flow[P, Coroutine[Any, Any, R]]] = None,
377
361
  flow_run: Optional[FlowRun] = None,
362
+ flow_run_id: Optional[UUID] = None,
378
363
  parameters: Optional[Dict[str, Any]] = None,
379
364
  wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
380
365
  return_type: Literal["state", "result"] = "result",
@@ -385,14 +370,14 @@ async def run_flow(
385
370
  We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
386
371
  """
387
372
 
388
- engine = FlowRunEngine[P, R](flow, parameters, flow_run)
373
+ engine = FlowRunEngine[P, R](flow, parameters, flow_run, flow_run_id)
389
374
 
390
375
  # This is a context manager that keeps track of the state of the flow run.
391
- async with engine.start() as run:
392
- await run.begin_run()
376
+ with engine.start() as run:
377
+ run.begin_run()
393
378
 
394
379
  while run.is_running():
395
- async with run.enter_run_context():
380
+ with run.enter_run_context():
396
381
  try:
397
382
  # This is where the flow is actually run.
398
383
  call_args, call_kwargs = parameters_to_args_kwargs(
@@ -400,15 +385,15 @@ async def run_flow(
400
385
  )
401
386
  result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
402
387
  # If the flow run is successful, finalize it.
403
- await run.handle_success(result)
388
+ run.handle_success(result)
404
389
 
405
390
  except Exception as exc:
406
391
  # If the flow fails, and we have retries left, set the flow to retrying.
407
- await run.handle_exception(exc)
392
+ run.handle_exception(exc)
408
393
 
409
394
  if return_type == "state":
410
395
  return run.state
411
- return await run.result()
396
+ return run.result()
412
397
 
413
398
 
414
399
  def run_flow_sync(
@@ -421,11 +406,11 @@ def run_flow_sync(
421
406
  engine = FlowRunEngine[P, R](flow, parameters, flow_run)
422
407
 
423
408
  # 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())
409
+ with engine.start() as run:
410
+ run.begin_run()
426
411
 
427
412
  while run.is_running():
428
- with run.enter_run_context_sync():
413
+ with run.enter_run_context():
429
414
  try:
430
415
  # This is where the flow is actually run.
431
416
  call_args, call_kwargs = parameters_to_args_kwargs(
@@ -433,12 +418,32 @@ def run_flow_sync(
433
418
  )
434
419
  result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
435
420
  # If the flow run is successful, finalize it.
436
- run_sync(run.handle_success(result))
421
+ run.handle_success(result)
437
422
 
438
423
  except Exception as exc:
439
424
  # If the flow fails, and we have retries left, set the flow to retrying.
440
- run_sync(run.handle_exception(exc))
425
+ run.handle_exception(exc)
441
426
 
442
427
  if return_type == "state":
443
428
  return run.state
444
- return run_sync(run.result())
429
+ return run.result()
430
+
431
+
432
+ def run_flow(
433
+ flow: Flow[P, R],
434
+ flow_run: Optional[FlowRun] = None,
435
+ parameters: Optional[Dict[str, Any]] = None,
436
+ wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
437
+ return_type: Literal["state", "result"] = "result",
438
+ ) -> Union[R, State, None]:
439
+ kwargs = dict(
440
+ flow=flow,
441
+ flow_run=flow_run,
442
+ parameters=parameters,
443
+ wait_for=wait_for,
444
+ return_type=return_type,
445
+ )
446
+ if flow.isasync:
447
+ return run_flow_async(**kwargs)
448
+ else:
449
+ return run_flow_sync(**kwargs)