prefect-client 3.1.3__py3-none-any.whl → 3.1.5__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/flow_engine.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  import logging
3
3
  import os
4
4
  import time
5
- from contextlib import ExitStack, contextmanager
5
+ from contextlib import ExitStack, asynccontextmanager, contextmanager
6
6
  from dataclasses import dataclass, field
7
7
  from typing import (
8
8
  Any,
@@ -22,19 +22,25 @@ from typing import (
22
22
  )
23
23
  from uuid import UUID
24
24
 
25
+ from anyio import CancelScope
25
26
  from opentelemetry import trace
26
27
  from opentelemetry.trace import Tracer, get_tracer
27
28
  from typing_extensions import ParamSpec
28
29
 
29
30
  import prefect
30
31
  from prefect import Task
31
- from prefect.client.orchestration import SyncPrefectClient, get_client
32
+ from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
32
33
  from prefect.client.schemas import FlowRun, TaskRun
33
34
  from prefect.client.schemas.filters import FlowRunFilter
34
35
  from prefect.client.schemas.sorting import FlowRunSort
35
36
  from prefect.concurrency.context import ConcurrencyContext
36
37
  from prefect.concurrency.v1.context import ConcurrencyContext as ConcurrencyContextV1
37
- from prefect.context import FlowRunContext, SyncClientContext, TagsContext
38
+ from prefect.context import (
39
+ AsyncClientContext,
40
+ FlowRunContext,
41
+ SyncClientContext,
42
+ TagsContext,
43
+ )
38
44
  from prefect.exceptions import (
39
45
  Abort,
40
46
  Pause,
@@ -79,6 +85,7 @@ from prefect.utilities.engine import (
79
85
  _resolve_custom_flow_run_name,
80
86
  capture_sigterm,
81
87
  link_state_to_result,
88
+ propose_state,
82
89
  propose_state_sync,
83
90
  resolve_to_final_result,
84
91
  )
@@ -112,7 +119,7 @@ def load_flow_and_flow_run(flow_run_id: UUID) -> Tuple[FlowRun, Flow]:
112
119
 
113
120
 
114
121
  @dataclass
115
- class FlowRunEngine(Generic[P, R]):
122
+ class BaseFlowRunEngine(Generic[P, R]):
116
123
  flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
117
124
  parameters: Optional[Dict[str, Any]] = None
118
125
  flow_run: Optional[FlowRun] = None
@@ -124,7 +131,6 @@ class FlowRunEngine(Generic[P, R]):
124
131
  # holds the exception raised by the user code, if any
125
132
  _raised: Union[Exception, Type[NotSet]] = NotSet
126
133
  _is_started: bool = False
127
- _client: Optional[SyncPrefectClient] = None
128
134
  short_circuit: bool = False
129
135
  _flow_run_name_set: bool = False
130
136
  _tracer: Tracer = field(
@@ -139,16 +145,50 @@ class FlowRunEngine(Generic[P, R]):
139
145
  if self.parameters is None:
140
146
  self.parameters = {}
141
147
 
148
+ @property
149
+ def state(self) -> State:
150
+ return self.flow_run.state # type: ignore
151
+
152
+ def _end_span_on_success(self):
153
+ if not self._span:
154
+ return
155
+ self._span.set_status(trace.Status(trace.StatusCode.OK))
156
+ self._span.end(time.time_ns())
157
+ self._span = None
158
+
159
+ def _end_span_on_error(self, exc: BaseException, description: Optional[str]):
160
+ if not self._span:
161
+ return
162
+ self._span.record_exception(exc)
163
+ self._span.set_status(trace.Status(trace.StatusCode.ERROR, description))
164
+ self._span.end(time.time_ns())
165
+ self._span = None
166
+
167
+ def is_running(self) -> bool:
168
+ if getattr(self, "flow_run", None) is None:
169
+ return False
170
+ return getattr(self, "flow_run").state.is_running()
171
+
172
+ def is_pending(self) -> bool:
173
+ if getattr(self, "flow_run", None) is None:
174
+ return False # TODO: handle this differently?
175
+ return getattr(self, "flow_run").state.is_pending()
176
+
177
+ def cancel_all_tasks(self):
178
+ if hasattr(self.flow.task_runner, "cancel_all"):
179
+ self.flow.task_runner.cancel_all() # type: ignore
180
+
181
+
182
+ @dataclass
183
+ class FlowRunEngine(BaseFlowRunEngine[P, R]):
184
+ _client: Optional[SyncPrefectClient] = None
185
+
142
186
  @property
143
187
  def client(self) -> SyncPrefectClient:
144
188
  if not self._is_started or self._client is None:
145
189
  raise RuntimeError("Engine has not started.")
146
190
  return self._client
147
191
 
148
- @property
149
- def state(self) -> State:
150
- return self.flow_run.state # type: ignore
151
-
152
192
  def _resolve_parameters(self):
153
193
  if not self.parameters:
154
194
  return {}
@@ -364,21 +404,6 @@ class FlowRunEngine(Generic[P, R]):
364
404
 
365
405
  self._end_span_on_error(exc, state.message)
366
406
 
367
- def _end_span_on_success(self):
368
- if not self._span:
369
- return
370
- self._span.set_status(trace.Status(trace.StatusCode.OK))
371
- self._span.end(time.time_ns())
372
- self._span = None
373
-
374
- def _end_span_on_error(self, exc: BaseException, description: Optional[str]):
375
- if not self._span:
376
- return
377
- self._span.record_exception(exc)
378
- self._span.set_status(trace.Status(trace.StatusCode.ERROR, description))
379
- self._span.end(time.time_ns())
380
- self._span = None
381
-
382
407
  def load_subflow_run(
383
408
  self,
384
409
  parent_task_run: TaskRun,
@@ -665,20 +690,6 @@ class FlowRunEngine(Generic[P, R]):
665
690
  self._is_started = False
666
691
  self._client = None
667
692
 
668
- def is_running(self) -> bool:
669
- if getattr(self, "flow_run", None) is None:
670
- return False
671
- return getattr(self, "flow_run").state.is_running()
672
-
673
- def is_pending(self) -> bool:
674
- if getattr(self, "flow_run", None) is None:
675
- return False # TODO: handle this differently?
676
- return getattr(self, "flow_run").state.is_pending()
677
-
678
- def cancel_all_tasks(self):
679
- if hasattr(self.flow.task_runner, "cancel_all"):
680
- self.flow.task_runner.cancel_all() # type: ignore
681
-
682
693
  # --------------------------
683
694
  #
684
695
  # The following methods compose the main task run loop
@@ -734,119 +745,685 @@ class FlowRunEngine(Generic[P, R]):
734
745
  self.handle_success(result)
735
746
 
736
747
 
737
- def run_flow_sync(
738
- flow: Flow[P, R],
739
- flow_run: Optional[FlowRun] = None,
740
- parameters: Optional[Dict[str, Any]] = None,
741
- wait_for: Optional[Iterable[PrefectFuture]] = None,
742
- return_type: Literal["state", "result"] = "result",
743
- ) -> Union[R, State, None]:
744
- engine = FlowRunEngine[P, R](
745
- flow=flow,
746
- parameters=parameters,
747
- flow_run=flow_run,
748
- wait_for=wait_for,
749
- )
748
+ @dataclass
749
+ class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
750
+ """
751
+ Async version of the flow run engine.
750
752
 
751
- with engine.start():
752
- while engine.is_running():
753
- with engine.run_context():
754
- engine.call_flow_fn()
753
+ NOTE: This has not been fully asyncified yet which may lead to async flows
754
+ not being fully asyncified.
755
+ """
755
756
 
756
- return engine.state if return_type == "state" else engine.result()
757
+ _client: Optional[PrefectClient] = None
757
758
 
759
+ @property
760
+ def client(self) -> PrefectClient:
761
+ if not self._is_started or self._client is None:
762
+ raise RuntimeError("Engine has not started.")
763
+ return self._client
758
764
 
759
- async def run_flow_async(
760
- flow: Flow[P, R],
761
- flow_run: Optional[FlowRun] = None,
762
- parameters: Optional[Dict[str, Any]] = None,
763
- wait_for: Optional[Iterable[PrefectFuture]] = None,
764
- return_type: Literal["state", "result"] = "result",
765
- ) -> Union[R, State, None]:
766
- engine = FlowRunEngine[P, R](
767
- flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
768
- )
765
+ def _resolve_parameters(self):
766
+ if not self.parameters:
767
+ return {}
769
768
 
770
- with engine.start():
771
- while engine.is_running():
772
- with engine.run_context():
773
- await engine.call_flow_fn()
769
+ resolved_parameters = {}
770
+ for parameter, value in self.parameters.items():
771
+ try:
772
+ resolved_parameters[parameter] = visit_collection(
773
+ value,
774
+ visit_fn=resolve_to_final_result,
775
+ return_data=True,
776
+ max_depth=-1,
777
+ remove_annotations=True,
778
+ context={},
779
+ )
780
+ except UpstreamTaskError:
781
+ raise
782
+ except Exception as exc:
783
+ raise PrefectException(
784
+ f"Failed to resolve inputs in parameter {parameter!r}. If your"
785
+ " parameter type is not supported, consider using the `quote`"
786
+ " annotation to skip resolution of inputs."
787
+ ) from exc
774
788
 
775
- return engine.state if return_type == "state" else engine.result()
789
+ self.parameters = resolved_parameters
776
790
 
791
+ def _wait_for_dependencies(self):
792
+ if not self.wait_for:
793
+ return
777
794
 
778
- def run_generator_flow_sync(
779
- flow: Flow[P, R],
780
- flow_run: Optional[FlowRun] = None,
781
- parameters: Optional[Dict[str, Any]] = None,
782
- wait_for: Optional[Iterable[PrefectFuture]] = None,
783
- return_type: Literal["state", "result"] = "result",
784
- ) -> Generator[R, None, None]:
785
- if return_type != "result":
786
- raise ValueError("The return_type for a generator flow must be 'result'")
795
+ visit_collection(
796
+ self.wait_for,
797
+ visit_fn=resolve_to_final_result,
798
+ return_data=False,
799
+ max_depth=-1,
800
+ remove_annotations=True,
801
+ context={},
802
+ )
787
803
 
788
- engine = FlowRunEngine[P, R](
789
- flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
790
- )
804
+ async def begin_run(self) -> State:
805
+ try:
806
+ self._resolve_parameters()
807
+ self._wait_for_dependencies()
808
+ except UpstreamTaskError as upstream_exc:
809
+ state = await self.set_state(
810
+ Pending(
811
+ name="NotReady",
812
+ message=str(upstream_exc),
813
+ ),
814
+ # if orchestrating a run already in a pending state, force orchestration to
815
+ # update the state name
816
+ force=self.state.is_pending(),
817
+ )
818
+ return state
791
819
 
792
- with engine.start():
793
- while engine.is_running():
794
- with engine.run_context():
795
- call_args, call_kwargs = parameters_to_args_kwargs(
796
- flow.fn, engine.parameters or {}
820
+ # validate prior to context so that context receives validated params
821
+ if self.flow.should_validate_parameters:
822
+ try:
823
+ self.parameters = self.flow.validate_parameters(self.parameters or {})
824
+ except Exception as exc:
825
+ message = "Validation of flow parameters failed with error:"
826
+ self.logger.error("%s %s", message, exc)
827
+ await self.handle_exception(
828
+ exc,
829
+ msg=message,
830
+ result_store=get_result_store().update_for_flow(
831
+ self.flow, _sync=True
832
+ ),
797
833
  )
798
- gen = flow.fn(*call_args, **call_kwargs)
799
- try:
800
- while True:
801
- gen_result = next(gen)
802
- # link the current state to the result for dependency tracking
803
- link_state_to_result(engine.state, gen_result)
804
- yield gen_result
805
- except StopIteration as exc:
806
- engine.handle_success(exc.value)
807
- except GeneratorExit as exc:
808
- engine.handle_success(None)
809
- gen.throw(exc)
834
+ self.short_circuit = True
835
+ await self.call_hooks()
810
836
 
811
- return engine.result()
837
+ new_state = Running()
838
+ state = await self.set_state(new_state)
839
+ while state.is_pending():
840
+ await asyncio.sleep(0.2)
841
+ state = await self.set_state(new_state)
842
+ return state
812
843
 
844
+ async def set_state(self, state: State, force: bool = False) -> State:
845
+ """ """
846
+ # prevents any state-setting activity
847
+ if self.short_circuit:
848
+ return self.state
813
849
 
814
- async def run_generator_flow_async(
815
- flow: Flow[P, R],
816
- flow_run: Optional[FlowRun] = None,
817
- parameters: Optional[Dict[str, Any]] = None,
818
- wait_for: Optional[Iterable[PrefectFuture]] = None,
819
- return_type: Literal["state", "result"] = "result",
820
- ) -> AsyncGenerator[R, None]:
821
- if return_type != "result":
822
- raise ValueError("The return_type for a generator flow must be 'result'")
850
+ state = await propose_state(
851
+ self.client, state, flow_run_id=self.flow_run.id, force=force
852
+ ) # type: ignore
853
+ self.flow_run.state = state # type: ignore
854
+ self.flow_run.state_name = state.name # type: ignore
855
+ self.flow_run.state_type = state.type # type: ignore
823
856
 
824
- engine = FlowRunEngine[P, R](
825
- flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
826
- )
857
+ if self._span:
858
+ self._span.add_event(
859
+ state.name,
860
+ {
861
+ "prefect.state.message": state.message or "",
862
+ "prefect.state.type": state.type,
863
+ "prefect.state.name": state.name or state.type,
864
+ "prefect.state.id": str(state.id),
865
+ },
866
+ )
867
+ return state
827
868
 
828
- with engine.start():
829
- while engine.is_running():
830
- with engine.run_context():
831
- call_args, call_kwargs = parameters_to_args_kwargs(
832
- flow.fn, engine.parameters or {}
833
- )
834
- gen = flow.fn(*call_args, **call_kwargs)
835
- try:
836
- while True:
837
- # can't use anext in Python < 3.10
838
- gen_result = await gen.__anext__()
839
- # link the current state to the result for dependency tracking
840
- link_state_to_result(engine.state, gen_result)
841
- yield gen_result
842
- except (StopAsyncIteration, GeneratorExit) as exc:
843
- engine.handle_success(None)
869
+ async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
870
+ if self._return_value is not NotSet and not isinstance(
871
+ self._return_value, State
872
+ ):
873
+ if isinstance(self._return_value, BaseResult):
874
+ _result = self._return_value.get()
875
+ else:
876
+ _result = self._return_value
877
+
878
+ if asyncio.iscoroutine(_result):
879
+ # getting the value for a BaseResult may return an awaitable
880
+ # depending on whether the parent frame is sync or not
881
+ _result = await _result
882
+ return _result
883
+
884
+ if self._raised is not NotSet:
885
+ if raise_on_failure:
886
+ raise self._raised
887
+ return self._raised
888
+
889
+ # This is a fall through case which leans on the existing state result mechanics to get the
890
+ # return value. This is necessary because we currently will return a State object if the
891
+ # the State was Prefect-created.
892
+ # TODO: Remove the need to get the result from a State except in cases where the return value
893
+ # is a State object.
894
+ _result = self.state.result(raise_on_failure=raise_on_failure, fetch=True) # type: ignore
895
+ # state.result is a `sync_compatible` function that may or may not return an awaitable
896
+ # depending on whether the parent frame is sync or not
897
+ if asyncio.iscoroutine(_result):
898
+ _result = await _result
899
+ return _result
900
+
901
+ async def handle_success(self, result: R) -> R:
902
+ result_store = getattr(FlowRunContext.get(), "result_store", None)
903
+ if result_store is None:
904
+ raise ValueError("Result store is not set")
905
+ resolved_result = resolve_futures_to_states(result)
906
+ terminal_state = await return_value_to_state(
907
+ resolved_result,
908
+ result_store=result_store,
909
+ write_result=should_persist_result(),
910
+ )
911
+ await self.set_state(terminal_state)
912
+ self._return_value = resolved_result
913
+
914
+ self._end_span_on_success()
915
+
916
+ return result
917
+
918
+ async def handle_exception(
919
+ self,
920
+ exc: Exception,
921
+ msg: Optional[str] = None,
922
+ result_store: Optional[ResultStore] = None,
923
+ ) -> State:
924
+ context = FlowRunContext.get()
925
+ terminal_state = cast(
926
+ State,
927
+ await exception_to_failed_state(
928
+ exc,
929
+ message=msg or "Flow run encountered an exception:",
930
+ result_store=result_store or getattr(context, "result_store", None),
931
+ write_result=True,
932
+ ),
933
+ )
934
+ state = await self.set_state(terminal_state)
935
+ if self.state.is_scheduled():
936
+ self.logger.info(
937
+ (
938
+ f"Received non-final state {state.name!r} when proposing final"
939
+ f" state {terminal_state.name!r} and will attempt to run again..."
940
+ ),
941
+ )
942
+ state = await self.set_state(Running())
943
+ self._raised = exc
944
+
945
+ self._end_span_on_error(exc, state.message)
946
+
947
+ return state
948
+
949
+ async def handle_timeout(self, exc: TimeoutError) -> None:
950
+ if isinstance(exc, FlowRunTimeoutError):
951
+ message = (
952
+ f"Flow run exceeded timeout of {self.flow.timeout_seconds} second(s)"
953
+ )
954
+ else:
955
+ message = f"Flow run failed due to timeout: {exc!r}"
956
+ self.logger.error(message)
957
+ state = Failed(
958
+ data=exc,
959
+ message=message,
960
+ name="TimedOut",
961
+ )
962
+ await self.set_state(state)
963
+ self._raised = exc
964
+
965
+ self._end_span_on_error(exc, message)
966
+
967
+ async def handle_crash(self, exc: BaseException) -> None:
968
+ # need to shield from asyncio cancellation to ensure we update the state
969
+ # on the server before exiting
970
+ with CancelScope(shield=True):
971
+ state = await exception_to_crashed_state(exc)
972
+ self.logger.error(f"Crash detected! {state.message}")
973
+ self.logger.debug("Crash details:", exc_info=exc)
974
+ await self.set_state(state, force=True)
975
+ self._raised = exc
976
+
977
+ self._end_span_on_error(exc, state.message)
978
+
979
+ async def load_subflow_run(
980
+ self,
981
+ parent_task_run: TaskRun,
982
+ client: PrefectClient,
983
+ context: FlowRunContext,
984
+ ) -> Union[FlowRun, None]:
985
+ """
986
+ This method attempts to load an existing flow run for a subflow task
987
+ run, if appropriate.
988
+
989
+ If the parent task run is in a final but not COMPLETED state, and not
990
+ being rerun, then we attempt to load an existing flow run instead of
991
+ creating a new one. This will prevent the engine from running the
992
+ subflow again.
993
+
994
+ If no existing flow run is found, or if the subflow should be rerun,
995
+ then no flow run is returned.
996
+ """
997
+
998
+ # check if the parent flow run is rerunning
999
+ rerunning = (
1000
+ context.flow_run.run_count > 1
1001
+ if getattr(context, "flow_run", None)
1002
+ and isinstance(context.flow_run, FlowRun)
1003
+ else False
1004
+ )
1005
+
1006
+ # if the parent task run is in a final but not completed state, and
1007
+ # not rerunning, then retrieve the most recent flow run instead of
1008
+ # creating a new one. This effectively loads a cached flow run for
1009
+ # situations where we are confident the flow should not be run
1010
+ # again.
1011
+ assert isinstance(parent_task_run.state, State)
1012
+ if parent_task_run.state.is_final() and not (
1013
+ rerunning and not parent_task_run.state.is_completed()
1014
+ ):
1015
+ # return the most recent flow run, if it exists
1016
+ flow_runs = await client.read_flow_runs(
1017
+ flow_run_filter=FlowRunFilter(
1018
+ parent_task_run_id={"any_": [parent_task_run.id]}
1019
+ ),
1020
+ sort=FlowRunSort.EXPECTED_START_TIME_ASC,
1021
+ limit=1,
1022
+ )
1023
+ if flow_runs:
1024
+ loaded_flow_run = flow_runs[-1]
1025
+ self._return_value = loaded_flow_run.state
1026
+ return loaded_flow_run
1027
+
1028
+ async def create_flow_run(self, client: PrefectClient) -> FlowRun:
1029
+ flow_run_ctx = FlowRunContext.get()
1030
+ parameters = self.parameters or {}
1031
+
1032
+ parent_task_run = None
1033
+
1034
+ # this is a subflow run
1035
+ if flow_run_ctx:
1036
+ # add a task to a parent flow run that represents the execution of a subflow run
1037
+ parent_task = Task(
1038
+ name=self.flow.name, fn=self.flow.fn, version=self.flow.version
1039
+ )
1040
+
1041
+ parent_task_run = await parent_task.create_run(
1042
+ flow_run_context=flow_run_ctx,
1043
+ parameters=self.parameters,
1044
+ wait_for=self.wait_for,
1045
+ )
1046
+
1047
+ # check if there is already a flow run for this subflow
1048
+ if subflow_run := await self.load_subflow_run(
1049
+ parent_task_run=parent_task_run, client=client, context=flow_run_ctx
1050
+ ):
1051
+ return subflow_run
1052
+
1053
+ flow_run = await client.create_flow_run(
1054
+ flow=self.flow,
1055
+ parameters=self.flow.serialize_parameters(parameters),
1056
+ state=Pending(),
1057
+ parent_task_run_id=getattr(parent_task_run, "id", None),
1058
+ tags=TagsContext.get().current_tags,
1059
+ )
1060
+ if flow_run_ctx:
1061
+ parent_logger = get_run_logger(flow_run_ctx)
1062
+ parent_logger.info(
1063
+ f"Created subflow run {flow_run.name!r} for flow {self.flow.name!r}"
1064
+ )
1065
+ else:
1066
+ self.logger.info(
1067
+ f"Created flow run {flow_run.name!r} for flow {self.flow.name!r}"
1068
+ )
1069
+
1070
+ return flow_run
1071
+
1072
+ async def call_hooks(self, state: Optional[State] = None):
1073
+ if state is None:
1074
+ state = self.state
1075
+ flow = self.flow
1076
+ flow_run = self.flow_run
1077
+
1078
+ if not flow_run:
1079
+ raise ValueError("Flow run is not set")
1080
+
1081
+ enable_cancellation_and_crashed_hooks = (
1082
+ os.environ.get(
1083
+ "PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS", "true"
1084
+ ).lower()
1085
+ == "true"
1086
+ )
1087
+
1088
+ if state.is_failed() and flow.on_failure_hooks:
1089
+ hooks = flow.on_failure_hooks
1090
+ elif state.is_completed() and flow.on_completion_hooks:
1091
+ hooks = flow.on_completion_hooks
1092
+ elif (
1093
+ enable_cancellation_and_crashed_hooks
1094
+ and state.is_cancelling()
1095
+ and flow.on_cancellation_hooks
1096
+ ):
1097
+ hooks = flow.on_cancellation_hooks
1098
+ elif (
1099
+ enable_cancellation_and_crashed_hooks
1100
+ and state.is_crashed()
1101
+ and flow.on_crashed_hooks
1102
+ ):
1103
+ hooks = flow.on_crashed_hooks
1104
+ elif state.is_running() and flow.on_running_hooks:
1105
+ hooks = flow.on_running_hooks
1106
+ else:
1107
+ hooks = None
1108
+
1109
+ for hook in hooks or []:
1110
+ hook_name = _get_hook_name(hook)
1111
+
1112
+ try:
1113
+ self.logger.info(
1114
+ f"Running hook {hook_name!r} in response to entering state"
1115
+ f" {state.name!r}"
1116
+ )
1117
+ result = hook(flow, flow_run, state)
1118
+ if asyncio.iscoroutine(result):
1119
+ await result
1120
+ except Exception:
1121
+ self.logger.error(
1122
+ f"An error was encountered while running hook {hook_name!r}",
1123
+ exc_info=True,
1124
+ )
1125
+ else:
1126
+ self.logger.info(f"Hook {hook_name!r} finished running successfully")
1127
+
1128
+ @asynccontextmanager
1129
+ async def setup_run_context(self, client: Optional[PrefectClient] = None):
1130
+ from prefect.utilities.engine import (
1131
+ should_log_prints,
1132
+ )
1133
+
1134
+ if client is None:
1135
+ client = self.client
1136
+ if not self.flow_run:
1137
+ raise ValueError("Flow run not set")
1138
+
1139
+ self.flow_run = await client.read_flow_run(self.flow_run.id)
1140
+ log_prints = should_log_prints(self.flow)
1141
+
1142
+ with ExitStack() as stack:
1143
+ # TODO: Explore closing task runner before completing the flow to
1144
+ # wait for futures to complete
1145
+ stack.enter_context(capture_sigterm())
1146
+ if log_prints:
1147
+ stack.enter_context(patch_print())
1148
+ task_runner = stack.enter_context(self.flow.task_runner.duplicate())
1149
+ stack.enter_context(
1150
+ FlowRunContext(
1151
+ flow=self.flow,
1152
+ log_prints=log_prints,
1153
+ flow_run=self.flow_run,
1154
+ parameters=self.parameters,
1155
+ client=client,
1156
+ result_store=get_result_store().update_for_flow(
1157
+ self.flow, _sync=True
1158
+ ),
1159
+ task_runner=task_runner,
1160
+ persist_result=self.flow.persist_result
1161
+ if self.flow.persist_result is not None
1162
+ else should_persist_result(),
1163
+ )
1164
+ )
1165
+ stack.enter_context(ConcurrencyContextV1())
1166
+ stack.enter_context(ConcurrencyContext())
1167
+
1168
+ # set the logger to the flow run logger
1169
+ self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
1170
+
1171
+ # update the flow run name if necessary
1172
+ if not self._flow_run_name_set and self.flow.flow_run_name:
1173
+ flow_run_name = _resolve_custom_flow_run_name(
1174
+ flow=self.flow, parameters=self.parameters
1175
+ )
1176
+ await self.client.set_flow_run_name(
1177
+ flow_run_id=self.flow_run.id, name=flow_run_name
1178
+ )
1179
+ self.logger.extra["flow_run_name"] = flow_run_name
1180
+ self.logger.debug(
1181
+ f"Renamed flow run {self.flow_run.name!r} to {flow_run_name!r}"
1182
+ )
1183
+ self.flow_run.name = flow_run_name
1184
+ self._flow_run_name_set = True
1185
+ yield
1186
+
1187
+ @asynccontextmanager
1188
+ async def initialize_run(self):
1189
+ """
1190
+ Enters a client context and creates a flow run if needed.
1191
+ """
1192
+ async with AsyncClientContext.get_or_create() as client_ctx:
1193
+ self._client = client_ctx.client
1194
+ self._is_started = True
1195
+
1196
+ if not self.flow_run:
1197
+ self.flow_run = await self.create_flow_run(self.client)
1198
+ flow_run_url = url_for(self.flow_run)
1199
+
1200
+ if flow_run_url:
1201
+ self.logger.info(
1202
+ f"View at {flow_run_url}", extra={"send_to_api": False}
1203
+ )
1204
+ else:
1205
+ # Update the empirical policy to match the flow if it is not set
1206
+ if self.flow_run.empirical_policy.retry_delay is None:
1207
+ self.flow_run.empirical_policy.retry_delay = (
1208
+ self.flow.retry_delay_seconds
1209
+ )
1210
+
1211
+ if self.flow_run.empirical_policy.retries is None:
1212
+ self.flow_run.empirical_policy.retries = self.flow.retries
1213
+
1214
+ await self.client.update_flow_run(
1215
+ flow_run_id=self.flow_run.id,
1216
+ flow_version=self.flow.version,
1217
+ empirical_policy=self.flow_run.empirical_policy,
1218
+ )
1219
+
1220
+ self._span = self._tracer.start_span(
1221
+ name=self.flow_run.name,
1222
+ attributes={
1223
+ **self.flow_run.labels,
1224
+ "prefect.run.type": "flow",
1225
+ "prefect.run.id": str(self.flow_run.id),
1226
+ "prefect.tags": self.flow_run.tags,
1227
+ "prefect.flow.name": self.flow.name,
1228
+ },
1229
+ )
1230
+
1231
+ try:
1232
+ yield self
1233
+
1234
+ except TerminationSignal as exc:
1235
+ self.cancel_all_tasks()
1236
+ await self.handle_crash(exc)
1237
+ raise
1238
+ except Exception:
1239
+ # regular exceptions are caught and re-raised to the user
1240
+ raise
1241
+ except (Abort, Pause):
1242
+ raise
1243
+ except GeneratorExit:
1244
+ # Do not capture generator exits as crashes
1245
+ raise
1246
+ except BaseException as exc:
1247
+ # BaseExceptions are caught and handled as crashes
1248
+ await self.handle_crash(exc)
1249
+ raise
1250
+ finally:
1251
+ # If debugging, use the more complete `repr` than the usual `str` description
1252
+ display_state = (
1253
+ repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
1254
+ )
1255
+ self.logger.log(
1256
+ level=logging.INFO if self.state.is_completed() else logging.ERROR,
1257
+ msg=f"Finished in state {display_state}",
1258
+ )
1259
+
1260
+ self._is_started = False
1261
+ self._client = None
1262
+
1263
+ # --------------------------
1264
+ #
1265
+ # The following methods compose the main task run loop
1266
+ #
1267
+ # --------------------------
1268
+
1269
+ @asynccontextmanager
1270
+ async def start(self) -> AsyncGenerator[None, None]:
1271
+ async with self.initialize_run():
1272
+ with trace.use_span(self._span):
1273
+ await self.begin_run()
1274
+
1275
+ if self.state.is_running():
1276
+ await self.call_hooks()
1277
+ yield
1278
+
1279
+ @asynccontextmanager
1280
+ async def run_context(self):
1281
+ timeout_context = timeout_async if self.flow.isasync else timeout
1282
+ # reenter the run context to ensure it is up to date for every run
1283
+ async with self.setup_run_context():
1284
+ try:
1285
+ with timeout_context(
1286
+ seconds=self.flow.timeout_seconds,
1287
+ timeout_exc_type=FlowRunTimeoutError,
1288
+ ):
1289
+ self.logger.debug(
1290
+ f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
1291
+ )
1292
+ yield self
1293
+ except TimeoutError as exc:
1294
+ await self.handle_timeout(exc)
1295
+ except Exception as exc:
1296
+ self.logger.exception("Encountered exception during execution: %r", exc)
1297
+ await self.handle_exception(exc)
1298
+ finally:
1299
+ if self.state.is_final() or self.state.is_cancelling():
1300
+ await self.call_hooks()
1301
+
1302
+ async def call_flow_fn(self) -> Coroutine[Any, Any, R]:
1303
+ """
1304
+ Convenience method to call the flow function. Returns a coroutine if the
1305
+ flow is async.
1306
+ """
1307
+ assert self.flow.isasync, "Flow must be async to be run with AsyncFlowRunEngine"
1308
+
1309
+ result = await call_with_parameters(self.flow.fn, self.parameters)
1310
+ await self.handle_success(result)
1311
+ return result
1312
+
1313
+
1314
+ def run_flow_sync(
1315
+ flow: Flow[P, R],
1316
+ flow_run: Optional[FlowRun] = None,
1317
+ parameters: Optional[Dict[str, Any]] = None,
1318
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
1319
+ return_type: Literal["state", "result"] = "result",
1320
+ ) -> Union[R, State, None]:
1321
+ engine = FlowRunEngine[P, R](
1322
+ flow=flow,
1323
+ parameters=parameters,
1324
+ flow_run=flow_run,
1325
+ wait_for=wait_for,
1326
+ )
1327
+
1328
+ with engine.start():
1329
+ while engine.is_running():
1330
+ with engine.run_context():
1331
+ engine.call_flow_fn()
1332
+
1333
+ return engine.state if return_type == "state" else engine.result()
1334
+
1335
+
1336
+ async def run_flow_async(
1337
+ flow: Flow[P, R],
1338
+ flow_run: Optional[FlowRun] = None,
1339
+ parameters: Optional[Dict[str, Any]] = None,
1340
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
1341
+ return_type: Literal["state", "result"] = "result",
1342
+ ) -> Union[R, State, None]:
1343
+ engine = AsyncFlowRunEngine[P, R](
1344
+ flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
1345
+ )
1346
+
1347
+ async with engine.start():
1348
+ while engine.is_running():
1349
+ async with engine.run_context():
1350
+ await engine.call_flow_fn()
1351
+
1352
+ return engine.state if return_type == "state" else await engine.result()
1353
+
1354
+
1355
+ def run_generator_flow_sync(
1356
+ flow: Flow[P, R],
1357
+ flow_run: Optional[FlowRun] = None,
1358
+ parameters: Optional[Dict[str, Any]] = None,
1359
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
1360
+ return_type: Literal["state", "result"] = "result",
1361
+ ) -> Generator[R, None, None]:
1362
+ if return_type != "result":
1363
+ raise ValueError("The return_type for a generator flow must be 'result'")
1364
+
1365
+ engine = FlowRunEngine[P, R](
1366
+ flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
1367
+ )
1368
+
1369
+ with engine.start():
1370
+ while engine.is_running():
1371
+ with engine.run_context():
1372
+ call_args, call_kwargs = parameters_to_args_kwargs(
1373
+ flow.fn, engine.parameters or {}
1374
+ )
1375
+ gen = flow.fn(*call_args, **call_kwargs)
1376
+ try:
1377
+ while True:
1378
+ gen_result = next(gen)
1379
+ # link the current state to the result for dependency tracking
1380
+ link_state_to_result(engine.state, gen_result)
1381
+ yield gen_result
1382
+ except StopIteration as exc:
1383
+ engine.handle_success(exc.value)
1384
+ except GeneratorExit as exc:
1385
+ engine.handle_success(None)
1386
+ gen.throw(exc)
1387
+
1388
+ return engine.result()
1389
+
1390
+
1391
+ async def run_generator_flow_async(
1392
+ flow: Flow[P, R],
1393
+ flow_run: Optional[FlowRun] = None,
1394
+ parameters: Optional[Dict[str, Any]] = None,
1395
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
1396
+ return_type: Literal["state", "result"] = "result",
1397
+ ) -> AsyncGenerator[R, None]:
1398
+ if return_type != "result":
1399
+ raise ValueError("The return_type for a generator flow must be 'result'")
1400
+
1401
+ engine = AsyncFlowRunEngine[P, R](
1402
+ flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
1403
+ )
1404
+
1405
+ async with engine.start():
1406
+ while engine.is_running():
1407
+ async with engine.run_context():
1408
+ call_args, call_kwargs = parameters_to_args_kwargs(
1409
+ flow.fn, engine.parameters or {}
1410
+ )
1411
+ gen = flow.fn(*call_args, **call_kwargs)
1412
+ try:
1413
+ while True:
1414
+ # can't use anext in Python < 3.10
1415
+ gen_result = await gen.__anext__()
1416
+ # link the current state to the result for dependency tracking
1417
+ link_state_to_result(engine.state, gen_result)
1418
+ yield gen_result
1419
+ except (StopAsyncIteration, GeneratorExit) as exc:
1420
+ await engine.handle_success(None)
844
1421
  if isinstance(exc, GeneratorExit):
845
1422
  gen.throw(exc)
846
1423
 
847
1424
  # async generators can't return, but we can raise failures here
848
1425
  if engine.state.is_failed():
849
- engine.result()
1426
+ await engine.result()
850
1427
 
851
1428
 
852
1429
  def run_flow(