prefect-client 3.0.0rc10__py3-none-any.whl → 3.0.0rc11__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/flows.py CHANGED
@@ -5,6 +5,7 @@ Module containing the base workflow class and decorator - for most use cases, us
5
5
  # This file requires type-checking with pyright because mypy does not yet support PEP612
6
6
  # See https://github.com/python/mypy/issues/8645
7
7
  import ast
8
+ import asyncio
8
9
  import datetime
9
10
  import importlib.util
10
11
  import inspect
@@ -28,6 +29,8 @@ from typing import (
28
29
  List,
29
30
  NoReturn,
30
31
  Optional,
32
+ Set,
33
+ Tuple,
31
34
  Type,
32
35
  TypeVar,
33
36
  Union,
@@ -44,7 +47,9 @@ from pydantic.v1.errors import ConfigError # TODO
44
47
  from rich.console import Console
45
48
  from typing_extensions import Literal, ParamSpec, Self
46
49
 
47
- from prefect._internal.compatibility.deprecated import deprecated_parameter
50
+ from prefect._internal.compatibility.deprecated import (
51
+ deprecated_parameter,
52
+ )
48
53
  from prefect._internal.concurrency.api import create_call, from_async
49
54
  from prefect.blocks.core import Block
50
55
  from prefect.client.orchestration import get_client
@@ -60,6 +65,7 @@ from prefect.exceptions import (
60
65
  MissingFlowError,
61
66
  ObjectNotFound,
62
67
  ParameterTypeError,
68
+ ScriptError,
63
69
  UnspecifiedFlowError,
64
70
  )
65
71
  from prefect.filesystems import LocalFileSystem, ReadableDeploymentStorage
@@ -778,8 +784,7 @@ class Flow(Generic[P, R]):
778
784
  self.on_failure_hooks.append(fn)
779
785
  return fn
780
786
 
781
- @sync_compatible
782
- async def serve(
787
+ def serve(
783
788
  self,
784
789
  name: Optional[str] = None,
785
790
  interval: Optional[
@@ -884,7 +889,7 @@ class Flow(Generic[P, R]):
884
889
  name = Path(name).stem
885
890
 
886
891
  runner = Runner(name=name, pause_on_shutdown=pause_on_shutdown, limit=limit)
887
- deployment_id = await runner.add_flow(
892
+ deployment_id = runner.add_flow(
888
893
  self,
889
894
  name=name,
890
895
  triggers=triggers,
@@ -917,15 +922,27 @@ class Flow(Generic[P, R]):
917
922
 
918
923
  console = Console()
919
924
  console.print(help_message, soft_wrap=True)
920
- await runner.start(webserver=webserver)
925
+
926
+ try:
927
+ loop = asyncio.get_running_loop()
928
+ except RuntimeError as exc:
929
+ if "no running event loop" in str(exc):
930
+ loop = None
931
+ else:
932
+ raise
933
+
934
+ if loop is not None:
935
+ loop.run_until_complete(runner.start(webserver=webserver))
936
+ else:
937
+ asyncio.run(runner.start(webserver=webserver))
921
938
 
922
939
  @classmethod
923
940
  @sync_compatible
924
941
  async def from_source(
925
- cls: Type[F],
942
+ cls: Type["Flow[P, R]"],
926
943
  source: Union[str, "RunnerStorage", ReadableDeploymentStorage],
927
944
  entrypoint: str,
928
- ) -> F:
945
+ ) -> "Flow[P, R]":
929
946
  """
930
947
  Loads a flow from a remote source.
931
948
 
@@ -1003,7 +1020,9 @@ class Flow(Generic[P, R]):
1003
1020
  create_storage_from_source,
1004
1021
  )
1005
1022
 
1006
- if isinstance(source, str):
1023
+ if isinstance(source, (Path, str)):
1024
+ if isinstance(source, Path):
1025
+ source = str(source)
1007
1026
  storage = create_storage_from_source(source)
1008
1027
  elif isinstance(source, RunnerStorage):
1009
1028
  storage = source
@@ -1186,9 +1205,9 @@ class Flow(Generic[P, R]):
1186
1205
  entrypoint_type=entrypoint_type,
1187
1206
  )
1188
1207
 
1189
- from prefect.deployments import runner
1208
+ from prefect.deployments.runner import deploy
1190
1209
 
1191
- deployment_ids = await runner.deploy(
1210
+ deployment_ids = await deploy(
1192
1211
  deployment,
1193
1212
  work_pool_name=work_pool_name,
1194
1213
  image=image,
@@ -1712,6 +1731,14 @@ def load_flow_from_entrypoint(
1712
1731
  raise MissingFlowError(
1713
1732
  f"Flow function with name {func_name!r} not found in {path!r}. "
1714
1733
  ) from exc
1734
+ except ScriptError as exc:
1735
+ # If the flow has dependencies that are not installed in the current
1736
+ # environment, fallback to loading the flow via AST parsing. The
1737
+ # drawback of this approach is that we're unable to actually load the
1738
+ # function, so we create a placeholder flow that will re-raise this
1739
+ # exception when called.
1740
+
1741
+ flow = load_placeholder_flow(entrypoint=entrypoint, raises=exc)
1715
1742
 
1716
1743
  if not isinstance(flow, Flow):
1717
1744
  raise MissingFlowError(
@@ -1722,14 +1749,13 @@ def load_flow_from_entrypoint(
1722
1749
  return flow
1723
1750
 
1724
1751
 
1725
- @sync_compatible
1726
- async def serve(
1752
+ def serve(
1727
1753
  *args: "RunnerDeployment",
1728
1754
  pause_on_shutdown: bool = True,
1729
1755
  print_starting_message: bool = True,
1730
1756
  limit: Optional[int] = None,
1731
1757
  **kwargs,
1732
- ) -> NoReturn:
1758
+ ):
1733
1759
  """
1734
1760
  Serve the provided list of deployments.
1735
1761
 
@@ -1779,7 +1805,7 @@ async def serve(
1779
1805
 
1780
1806
  runner = Runner(pause_on_shutdown=pause_on_shutdown, limit=limit, **kwargs)
1781
1807
  for deployment in args:
1782
- await runner.add_deployment(deployment)
1808
+ runner.add_deployment(deployment)
1783
1809
 
1784
1810
  if print_starting_message:
1785
1811
  help_message_top = (
@@ -1810,7 +1836,18 @@ async def serve(
1810
1836
  Group(help_message_top, table, help_message_bottom), soft_wrap=True
1811
1837
  )
1812
1838
 
1813
- await runner.start()
1839
+ try:
1840
+ loop = asyncio.get_running_loop()
1841
+ except RuntimeError as exc:
1842
+ if "no running event loop" in str(exc):
1843
+ loop = None
1844
+ else:
1845
+ raise
1846
+
1847
+ if loop is not None:
1848
+ loop.run_until_complete(runner.start())
1849
+ else:
1850
+ asyncio.run(runner.start())
1814
1851
 
1815
1852
 
1816
1853
  @client_injector
@@ -1892,24 +1929,138 @@ async def load_flow_from_flow_run(
1892
1929
  return flow
1893
1930
 
1894
1931
 
1895
- def load_flow_argument_from_entrypoint(
1896
- entrypoint: str, arg: str = "name"
1897
- ) -> Optional[str]:
1932
+ def load_placeholder_flow(entrypoint: str, raises: Exception):
1898
1933
  """
1899
- Extract a flow argument from an entrypoint string.
1934
+ Load a placeholder flow that is initialized with the same arguments as the
1935
+ flow specified in the entrypoint. If called the flow will raise `raises`.
1900
1936
 
1901
- Loads the source code of the entrypoint and extracts the flow argument from the
1902
- `flow` decorator.
1937
+ This is useful when a flow can't be loaded due to missing dependencies or
1938
+ other issues but the base metadata defining the flow is still needed.
1903
1939
 
1904
1940
  Args:
1905
- entrypoint: a string in the format `<path_to_script>:<flow_func_name>` or a module path
1906
- to a flow function
1941
+ entrypoint: a string in the format `<path_to_script>:<flow_func_name>`
1942
+ or a module path to a flow function
1943
+ raises: an exception to raise when the flow is called
1944
+ """
1945
+
1946
+ def _base_placeholder():
1947
+ raise raises
1948
+
1949
+ def sync_placeholder_flow(*args, **kwargs):
1950
+ _base_placeholder()
1951
+
1952
+ async def async_placeholder_flow(*args, **kwargs):
1953
+ _base_placeholder()
1954
+
1955
+ placeholder_flow = (
1956
+ async_placeholder_flow
1957
+ if is_entrypoint_async(entrypoint)
1958
+ else sync_placeholder_flow
1959
+ )
1960
+
1961
+ arguments = load_flow_arguments_from_entrypoint(entrypoint)
1962
+ arguments["fn"] = placeholder_flow
1963
+
1964
+ return Flow(**arguments)
1965
+
1966
+
1967
+ def load_flow_arguments_from_entrypoint(
1968
+ entrypoint: str, arguments: Optional[Union[List[str], Set[str]]] = None
1969
+ ) -> dict[str, Any]:
1970
+ """
1971
+ Extract flow arguments from an entrypoint string.
1972
+
1973
+ Loads the source code of the entrypoint and extracts the flow arguments
1974
+ from the `flow` decorator.
1975
+
1976
+ Args:
1977
+ entrypoint: a string in the format `<path_to_script>:<flow_func_name>`
1978
+ or a module path to a flow function
1979
+ """
1980
+
1981
+ func_def, source_code = _entrypoint_definition_and_source(entrypoint)
1982
+
1983
+ if arguments is None:
1984
+ # If no arguments are provided default to known arguments that are of
1985
+ # built-in types.
1986
+ arguments = {
1987
+ "name",
1988
+ "version",
1989
+ "retries",
1990
+ "retry_delay_seconds",
1991
+ "description",
1992
+ "timeout_seconds",
1993
+ "validate_parameters",
1994
+ "persist_result",
1995
+ "cache_result_in_memory",
1996
+ "log_prints",
1997
+ }
1998
+
1999
+ result = {}
2000
+
2001
+ for decorator in func_def.decorator_list:
2002
+ if (
2003
+ isinstance(decorator, ast.Call)
2004
+ and getattr(decorator.func, "id", "") == "flow"
2005
+ ):
2006
+ for keyword in decorator.keywords:
2007
+ if keyword.arg not in arguments:
2008
+ continue
2009
+
2010
+ if isinstance(keyword.value, ast.Constant):
2011
+ # Use the string value of the argument
2012
+ result[keyword.arg] = str(keyword.value.value)
2013
+ continue
2014
+
2015
+ # if the arg value is not a raw str (i.e. a variable or expression),
2016
+ # then attempt to evaluate it
2017
+ namespace = safe_load_namespace(source_code)
2018
+ literal_arg_value = ast.get_source_segment(source_code, keyword.value)
2019
+ cleaned_value = (
2020
+ literal_arg_value.replace("\n", "") if literal_arg_value else ""
2021
+ )
2022
+
2023
+ try:
2024
+ evaluated_value = eval(cleaned_value, namespace) # type: ignore
2025
+ result[keyword.arg] = str(evaluated_value)
2026
+ except Exception as e:
2027
+ logger.info(
2028
+ "Failed to parse @flow argument: `%s=%s` due to the following error. Ignoring and falling back to default behavior.",
2029
+ keyword.arg,
2030
+ literal_arg_value,
2031
+ exc_info=e,
2032
+ )
2033
+ # ignore the decorator arg and fallback to default behavior
2034
+ continue
2035
+
2036
+ if "name" in arguments and "name" not in result:
2037
+ # If no matching decorator or keyword argument for `name' is found
2038
+ # fallback to the function name.
2039
+ result["name"] = func_def.name.replace("_", "-")
2040
+
2041
+ return result
2042
+
2043
+
2044
+ def is_entrypoint_async(entrypoint: str) -> bool:
2045
+ """
2046
+ Determine if the function specified in the entrypoint is asynchronous.
2047
+
2048
+ Args:
2049
+ entrypoint: A string in the format `<path_to_script>:<func_name>` or
2050
+ a module path to a function.
1907
2051
 
1908
2052
  Returns:
1909
- The flow argument value
2053
+ True if the function is asynchronous, False otherwise.
1910
2054
  """
2055
+ func_def, _ = _entrypoint_definition_and_source(entrypoint)
2056
+ return isinstance(func_def, ast.AsyncFunctionDef)
2057
+
2058
+
2059
+ def _entrypoint_definition_and_source(
2060
+ entrypoint: str,
2061
+ ) -> Tuple[Union[ast.FunctionDef, ast.AsyncFunctionDef], str]:
1911
2062
  if ":" in entrypoint:
1912
- # split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
2063
+ # Split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
1913
2064
  path, func_name = entrypoint.rsplit(":", maxsplit=1)
1914
2065
  source_code = Path(path).read_text()
1915
2066
  else:
@@ -1918,6 +2069,7 @@ def load_flow_argument_from_entrypoint(
1918
2069
  if not spec or not spec.origin:
1919
2070
  raise ValueError(f"Could not find module {path!r}")
1920
2071
  source_code = Path(spec.origin).read_text()
2072
+
1921
2073
  parsed_code = ast.parse(source_code)
1922
2074
  func_def = next(
1923
2075
  (
@@ -1934,46 +2086,8 @@ def load_flow_argument_from_entrypoint(
1934
2086
  ),
1935
2087
  None,
1936
2088
  )
2089
+
1937
2090
  if not func_def:
1938
2091
  raise ValueError(f"Could not find flow {func_name!r} in {path!r}")
1939
- for decorator in func_def.decorator_list:
1940
- if (
1941
- isinstance(decorator, ast.Call)
1942
- and getattr(decorator.func, "id", "") == "flow"
1943
- ):
1944
- for keyword in decorator.keywords:
1945
- if keyword.arg == arg:
1946
- if isinstance(keyword.value, ast.Constant):
1947
- return (
1948
- keyword.value.value
1949
- ) # Return the string value of the argument
1950
-
1951
- # if the arg value is not a raw str (i.e. a variable or expression),
1952
- # then attempt to evaluate it
1953
- namespace = safe_load_namespace(source_code)
1954
- literal_arg_value = ast.get_source_segment(
1955
- source_code, keyword.value
1956
- )
1957
- cleaned_value = (
1958
- literal_arg_value.replace("\n", "") if literal_arg_value else ""
1959
- )
1960
-
1961
- try:
1962
- evaluated_value = eval(cleaned_value, namespace) # type: ignore
1963
- except Exception as e:
1964
- logger.info(
1965
- "Failed to parse @flow argument: `%s=%s` due to the following error. Ignoring and falling back to default behavior.",
1966
- arg,
1967
- literal_arg_value,
1968
- exc_info=e,
1969
- )
1970
- # ignore the decorator arg and fallback to default behavior
1971
- break
1972
- return str(evaluated_value)
1973
-
1974
- if arg == "name":
1975
- return func_name.replace(
1976
- "_", "-"
1977
- ) # If no matching decorator or keyword argument is found
1978
2092
 
1979
- return None
2093
+ return func_def, source_code
prefect/futures.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import collections
2
3
  import concurrent.futures
3
4
  import inspect
4
5
  import uuid
@@ -256,13 +257,7 @@ class PrefectFutureList(list, Iterator, Generic[F]):
256
257
  timeout: The maximum number of seconds to wait for all futures to
257
258
  complete. This method will not raise if the timeout is reached.
258
259
  """
259
- try:
260
- with timeout_context(timeout):
261
- for future in self:
262
- future.wait()
263
- except TimeoutError:
264
- logger.debug("Timed out waiting for all futures to complete.")
265
- return
260
+ wait(self, timeout=timeout)
266
261
 
267
262
  def result(
268
263
  self,
@@ -297,6 +292,57 @@ class PrefectFutureList(list, Iterator, Generic[F]):
297
292
  ) from exc
298
293
 
299
294
 
295
+ DoneAndNotDoneFutures = collections.namedtuple("DoneAndNotDoneFutures", "done not_done")
296
+
297
+
298
+ def wait(futures: List[PrefectFuture], timeout=None) -> DoneAndNotDoneFutures:
299
+ """
300
+ Wait for the futures in the given sequence to complete.
301
+
302
+ Args:
303
+ futures: The sequence of Futures to wait upon.
304
+ timeout: The maximum number of seconds to wait. If None, then there
305
+ is no limit on the wait time.
306
+
307
+ Returns:
308
+ A named 2-tuple of sets. The first set, named 'done', contains the
309
+ futures that completed (is finished or cancelled) before the wait
310
+ completed. The second set, named 'not_done', contains uncompleted
311
+ futures. Duplicate futures given to *futures* are removed and will be
312
+ returned only once.
313
+
314
+ Examples:
315
+ ```python
316
+ @task
317
+ def sleep_task(seconds):
318
+ sleep(seconds)
319
+ return 42
320
+
321
+ @flow
322
+ def flow():
323
+ futures = random_task.map(range(10))
324
+ done, not_done = wait(futures, timeout=5)
325
+ print(f"Done: {len(done)}")
326
+ print(f"Not Done: {len(not_done)}")
327
+ ```
328
+ """
329
+ futures = set(futures)
330
+ done = {f for f in futures if f._final_state}
331
+ not_done = futures - done
332
+ if len(done) == len(futures):
333
+ return DoneAndNotDoneFutures(done, not_done)
334
+ try:
335
+ with timeout_context(timeout):
336
+ for future in not_done.copy():
337
+ future.wait()
338
+ done.add(future)
339
+ not_done.remove(future)
340
+ return DoneAndNotDoneFutures(done, not_done)
341
+ except TimeoutError:
342
+ logger.debug("Timed out waiting for all futures to complete.")
343
+ return DoneAndNotDoneFutures(done, not_done)
344
+
345
+
300
346
  def resolve_futures_to_states(
301
347
  expr: Union[PrefectFuture, Any],
302
348
  ) -> Union[State, Any]:
@@ -97,7 +97,7 @@ def get_logger(name: Optional[str] = None) -> logging.Logger:
97
97
 
98
98
 
99
99
  def get_run_logger(
100
- context: "RunContext" = None, **kwargs: str
100
+ context: Optional["RunContext"] = None, **kwargs: str
101
101
  ) -> Union[logging.Logger, logging.LoggerAdapter]:
102
102
  """
103
103
  Get a Prefect logger for the current task run or flow run.
prefect/runner/runner.py CHANGED
@@ -65,17 +65,13 @@ from prefect.client.schemas.filters import (
65
65
  FlowRunFilterStateName,
66
66
  FlowRunFilterStateType,
67
67
  )
68
- from prefect.client.schemas.objects import (
69
- FlowRun,
70
- State,
71
- StateType,
72
- )
68
+ from prefect.client.schemas.objects import Flow as APIFlow
69
+ from prefect.client.schemas.objects import FlowRun, State, StateType
73
70
  from prefect.client.schemas.schedules import SCHEDULE_TYPES
74
- from prefect.deployments.runner import (
75
- EntrypointType,
76
- RunnerDeployment,
77
- )
78
71
  from prefect.events import DeploymentTriggerTypes, TriggerTypes
72
+ from prefect.events.related import tags_as_related_resources
73
+ from prefect.events.schemas.events import RelatedResource
74
+ from prefect.events.utilities import emit_event
79
75
  from prefect.exceptions import Abort, ObjectNotFound
80
76
  from prefect.flows import Flow, load_flow_from_flow_run
81
77
  from prefect.logging.loggers import PrefectLogAdapter, flow_run_logger, get_logger
@@ -88,6 +84,7 @@ from prefect.settings import (
88
84
  get_current_settings,
89
85
  )
90
86
  from prefect.states import Crashed, Pending, exception_to_failed_state
87
+ from prefect.types.entrypoint import EntrypointType
91
88
  from prefect.utilities.asyncutils import (
92
89
  asyncnullcontext,
93
90
  is_async_fn,
@@ -96,9 +93,12 @@ from prefect.utilities.asyncutils import (
96
93
  from prefect.utilities.engine import propose_state
97
94
  from prefect.utilities.processutils import _register_signal, run_process
98
95
  from prefect.utilities.services import critical_service_loop
96
+ from prefect.utilities.slugify import slugify
99
97
 
100
98
  if TYPE_CHECKING:
99
+ from prefect.client.schemas.objects import Deployment
101
100
  from prefect.client.types.flexible_schedule_list import FlexibleScheduleList
101
+ from prefect.deployments.runner import RunnerDeployment
102
102
 
103
103
  __all__ = ["Runner"]
104
104
 
@@ -130,6 +130,7 @@ class Runner:
130
130
  Examples:
131
131
  Set up a Runner to manage the execute of scheduled flow runs for two flows:
132
132
  ```python
133
+ import asyncio
133
134
  from prefect import flow, Runner
134
135
 
135
136
  @flow
@@ -149,7 +150,7 @@ class Runner:
149
150
  # Run on a cron schedule
150
151
  runner.add_flow(goodbye_flow, schedule={"cron": "0 * * * *"})
151
152
 
152
- runner.start()
153
+ asyncio.run(runner.start())
153
154
  ```
154
155
  """
155
156
  if name and ("/" in name or "%" in name):
@@ -166,9 +167,6 @@ class Runner:
166
167
  self.query_seconds = query_seconds or PREFECT_RUNNER_POLL_FREQUENCY.value()
167
168
  self._prefetch_seconds = prefetch_seconds
168
169
 
169
- self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
170
- self._loops_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
171
-
172
170
  self._limiter: Optional[anyio.CapacityLimiter] = anyio.CapacityLimiter(
173
171
  self.limit
174
172
  )
@@ -177,19 +175,20 @@ class Runner:
177
175
  self._cancelling_flow_run_ids = set()
178
176
  self._scheduled_task_scopes = set()
179
177
  self._deployment_ids: Set[UUID] = set()
180
- self._flow_run_process_map = dict()
178
+ self._flow_run_process_map: Dict[UUID, Dict] = dict()
181
179
 
182
180
  self._tmp_dir: Path = (
183
181
  Path(tempfile.gettempdir()) / "runner_storage" / str(uuid4())
184
182
  )
185
183
  self._storage_objs: List[RunnerStorage] = []
186
184
  self._deployment_storage_map: Dict[UUID, RunnerStorage] = {}
187
- self._loop = asyncio.get_event_loop()
185
+
186
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
188
187
 
189
188
  @sync_compatible
190
189
  async def add_deployment(
191
190
  self,
192
- deployment: RunnerDeployment,
191
+ deployment: "RunnerDeployment",
193
192
  ) -> UUID:
194
193
  """
195
194
  Registers the deployment with the Prefect API and will monitor for work once
@@ -324,7 +323,6 @@ class Runner:
324
323
 
325
324
  sys.exit(0)
326
325
 
327
- @sync_compatible
328
326
  async def start(
329
327
  self, run_once: bool = False, webserver: Optional[bool] = None
330
328
  ) -> None:
@@ -342,6 +340,7 @@ class Runner:
342
340
  Initialize a Runner, add two flows, and serve them by starting the Runner:
343
341
 
344
342
  ```python
343
+ import asyncio
345
344
  from prefect import flow, Runner
346
345
 
347
346
  @flow
@@ -361,7 +360,7 @@ class Runner:
361
360
  # Run on a cron schedule
362
361
  runner.add_flow(goodbye_flow, schedule={"cron": "0 * * * *"})
363
362
 
364
- runner.start()
363
+ asyncio.run(runner.start())
365
364
  ```
366
365
  """
367
366
  from prefect.runner.server import start_webserver
@@ -695,8 +694,9 @@ class Runner:
695
694
  """
696
695
  self._logger.info("Pausing all deployments...")
697
696
  for deployment_id in self._deployment_ids:
698
- self._logger.debug(f"Pausing deployment '{deployment_id}'")
699
697
  await self._client.set_deployment_paused_state(deployment_id, True)
698
+ self._logger.debug(f"Paused deployment '{deployment_id}'")
699
+
700
700
  self._logger.info("All deployments have been paused!")
701
701
 
702
702
  async def _get_and_submit_flow_runs(self):
@@ -818,8 +818,71 @@ class Runner:
818
818
  "message": state_msg or "Flow run was cancelled successfully."
819
819
  },
820
820
  )
821
+ try:
822
+ deployment = await self._client.read_deployment(flow_run.deployment_id)
823
+ except ObjectNotFound:
824
+ deployment = None
825
+ try:
826
+ flow = await self._client.read_flow(flow_run.flow_id)
827
+ except ObjectNotFound:
828
+ flow = None
829
+ self._emit_flow_run_cancelled_event(
830
+ flow_run=flow_run, flow=flow, deployment=deployment
831
+ )
821
832
  run_logger.info(f"Cancelled flow run '{flow_run.name}'!")
822
833
 
834
+ def _event_resource(self):
835
+ from prefect import __version__
836
+
837
+ return {
838
+ "prefect.resource.id": f"prefect.runner.{slugify(self.name)}",
839
+ "prefect.resource.name": self.name,
840
+ "prefect.version": __version__,
841
+ }
842
+
843
+ def _emit_flow_run_cancelled_event(
844
+ self,
845
+ flow_run: "FlowRun",
846
+ flow: "Optional[APIFlow]",
847
+ deployment: "Optional[Deployment]",
848
+ ):
849
+ related = []
850
+ tags = []
851
+ if deployment:
852
+ related.append(
853
+ {
854
+ "prefect.resource.id": f"prefect.deployment.{deployment.id}",
855
+ "prefect.resource.role": "deployment",
856
+ "prefect.resource.name": deployment.name,
857
+ }
858
+ )
859
+ tags.extend(deployment.tags)
860
+ if flow:
861
+ related.append(
862
+ {
863
+ "prefect.resource.id": f"prefect.flow.{flow.id}",
864
+ "prefect.resource.role": "flow",
865
+ "prefect.resource.name": flow.name,
866
+ }
867
+ )
868
+ related.append(
869
+ {
870
+ "prefect.resource.id": f"prefect.flow-run.{flow_run.id}",
871
+ "prefect.resource.role": "flow-run",
872
+ "prefect.resource.name": flow_run.name,
873
+ }
874
+ )
875
+ tags.extend(flow_run.tags)
876
+
877
+ related = [RelatedResource.model_validate(r) for r in related]
878
+ related += tags_as_related_resources(set(tags))
879
+
880
+ emit_event(
881
+ event="prefect.runner.cancelled-flow-run",
882
+ resource=self._event_resource(),
883
+ related=related,
884
+ )
885
+
823
886
  async def _get_scheduled_flow_runs(
824
887
  self,
825
888
  ) -> List["FlowRun"]:
@@ -956,7 +1019,7 @@ class Runner:
956
1019
  # If the run is not ready to submit, release the concurrency slot
957
1020
  self._release_limit_slot(flow_run.id)
958
1021
 
959
- self._submitting_flow_run_ids.remove(flow_run.id)
1022
+ self._submitting_flow_run_ids.discard(flow_run.id)
960
1023
 
961
1024
  async def _submit_run_and_capture_errors(
962
1025
  self,
@@ -1163,6 +1226,16 @@ class Runner:
1163
1226
  self._logger.debug("Starting runner...")
1164
1227
  self._client = get_client()
1165
1228
  self._tmp_dir.mkdir(parents=True)
1229
+
1230
+ if not hasattr(self, "_loop") or not self._loop:
1231
+ self._loop = asyncio.get_event_loop()
1232
+
1233
+ if not hasattr(self, "_runs_task_group") or not self._runs_task_group:
1234
+ self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
1235
+
1236
+ if not hasattr(self, "_loops_task_group") or not self._loops_task_group:
1237
+ self._loops_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
1238
+
1166
1239
  await self._client.__aenter__()
1167
1240
  await self._runs_task_group.__aenter__()
1168
1241