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/_internal/concurrency/api.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/client/cloud.py +1 -1
- prefect/client/schemas/objects.py +1 -1
- prefect/concurrency/asyncio.py +3 -3
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +3 -2
- prefect/concurrency/sync.py +19 -5
- prefect/context.py +8 -2
- prefect/deployments/__init__.py +28 -15
- prefect/deployments/steps/pull.py +7 -0
- prefect/flow_engine.py +5 -7
- prefect/flows.py +179 -65
- prefect/futures.py +53 -7
- prefect/logging/loggers.py +1 -1
- prefect/runner/runner.py +93 -20
- prefect/runner/server.py +20 -22
- prefect/runner/submit.py +0 -8
- prefect/runtime/flow_run.py +38 -3
- prefect/settings.py +9 -13
- prefect/task_worker.py +1 -1
- prefect/transactions.py +16 -0
- prefect/utilities/asyncutils.py +1 -0
- prefect/utilities/engine.py +34 -1
- prefect/workers/base.py +98 -208
- prefect/workers/process.py +262 -4
- prefect/workers/server.py +27 -9
- {prefect_client-3.0.0rc10.dist-info → prefect_client-3.0.0rc11.dist-info}/METADATA +3 -3
- {prefect_client-3.0.0rc10.dist-info → prefect_client-3.0.0rc11.dist-info}/RECORD +32 -31
- {prefect_client-3.0.0rc10.dist-info → prefect_client-3.0.0rc11.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc10.dist-info → prefect_client-3.0.0rc11.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc10.dist-info → prefect_client-3.0.0rc11.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
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 =
|
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
|
-
|
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[
|
942
|
+
cls: Type["Flow[P, R]"],
|
926
943
|
source: Union[str, "RunnerStorage", ReadableDeploymentStorage],
|
927
944
|
entrypoint: str,
|
928
|
-
) ->
|
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
|
1208
|
+
from prefect.deployments.runner import deploy
|
1190
1209
|
|
1191
|
-
deployment_ids = await
|
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
|
-
|
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
|
-
)
|
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
|
-
|
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
|
-
|
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
|
1896
|
-
entrypoint: str, arg: str = "name"
|
1897
|
-
) -> Optional[str]:
|
1932
|
+
def load_placeholder_flow(entrypoint: str, raises: Exception):
|
1898
1933
|
"""
|
1899
|
-
|
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
|
-
|
1902
|
-
|
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>`
|
1906
|
-
|
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
|
-
|
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
|
-
#
|
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
|
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
|
-
|
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]:
|
prefect/logging/loggers.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
|