prefect-client 3.3.5.dev3__py3-none-any.whl → 3.3.6__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/_build_info.py +3 -3
- prefect/_experimental/{bundles.py → bundles/__init__.py} +39 -11
- prefect/_experimental/bundles/execute.py +34 -0
- prefect/_versioning.py +180 -0
- prefect/cache_policies.py +4 -4
- prefect/client/orchestration/_deployments/client.py +49 -22
- prefect/concurrency/context.py +3 -1
- prefect/context.py +1 -3
- prefect/deployments/runner.py +22 -6
- prefect/events/clients.py +1 -1
- prefect/flows.py +240 -1
- prefect/futures.py +54 -21
- prefect/runner/runner.py +8 -3
- prefect/server/api/deployments.py +41 -3
- prefect/utilities/_ast.py +117 -0
- prefect/workers/base.py +193 -7
- prefect/workers/process.py +60 -3
- {prefect_client-3.3.5.dev3.dist-info → prefect_client-3.3.6.dist-info}/METADATA +2 -2
- {prefect_client-3.3.5.dev3.dist-info → prefect_client-3.3.6.dist-info}/RECORD +21 -18
- {prefect_client-3.3.5.dev3.dist-info → prefect_client-3.3.6.dist-info}/WHEEL +0 -0
- {prefect_client-3.3.5.dev3.dist-info → prefect_client-3.3.6.dist-info}/licenses/LICENSE +0 -0
prefect/flows.py
CHANGED
@@ -40,6 +40,7 @@ from typing import (
|
|
40
40
|
from uuid import UUID
|
41
41
|
|
42
42
|
import pydantic
|
43
|
+
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
43
44
|
from pydantic.v1 import BaseModel as V1BaseModel
|
44
45
|
from pydantic.v1.decorator import ValidatedFunction as V1ValidatedFunction
|
45
46
|
from pydantic.v1.errors import ConfigError # TODO
|
@@ -64,7 +65,7 @@ from prefect.exceptions import (
|
|
64
65
|
UnspecifiedFlowError,
|
65
66
|
)
|
66
67
|
from prefect.filesystems import LocalFileSystem, ReadableDeploymentStorage
|
67
|
-
from prefect.futures import PrefectFuture
|
68
|
+
from prefect.futures import PrefectFlowRunFuture, PrefectFuture
|
68
69
|
from prefect.logging import get_logger
|
69
70
|
from prefect.logging.loggers import flow_run_logger
|
70
71
|
from prefect.results import ResultSerializer, ResultStorage
|
@@ -105,6 +106,9 @@ from ._internal.pydantic.v2_validated_func import (
|
|
105
106
|
V2ValidatedFunction as ValidatedFunction,
|
106
107
|
)
|
107
108
|
|
109
|
+
if TYPE_CHECKING:
|
110
|
+
from prefect.workers.base import BaseWorker
|
111
|
+
|
108
112
|
T = TypeVar("T") # Generic type var for capturing the inner return type of async funcs
|
109
113
|
R = TypeVar("R") # The return type of the user's function
|
110
114
|
P = ParamSpec("P") # The parameters of the flow
|
@@ -1984,6 +1988,241 @@ class FlowDecorator:
|
|
1984
1988
|
flow: FlowDecorator = FlowDecorator()
|
1985
1989
|
|
1986
1990
|
|
1991
|
+
class InfrastructureBoundFlow(Flow[P, R]):
|
1992
|
+
"""
|
1993
|
+
EXPERIMENTAL: This class is experimental and may be removed or changed in future
|
1994
|
+
releases.
|
1995
|
+
|
1996
|
+
A flow that is bound to running on a specific infrastructure.
|
1997
|
+
|
1998
|
+
Attributes:
|
1999
|
+
work_pool: The name of the work pool to run the flow on. The base job
|
2000
|
+
configuration of the work pool will determine the configuration of the
|
2001
|
+
infrastructure the flow will run on.
|
2002
|
+
job_variables: Infrastructure configuration that will override the base job
|
2003
|
+
configuration of the work pool.
|
2004
|
+
worker_cls: The class of the worker to use to spin up infrastructure and submit
|
2005
|
+
the flow to it.
|
2006
|
+
"""
|
2007
|
+
|
2008
|
+
def __init__(
|
2009
|
+
self,
|
2010
|
+
*args: Any,
|
2011
|
+
work_pool: str,
|
2012
|
+
job_variables: dict[str, Any],
|
2013
|
+
worker_cls: type["BaseWorker[Any, Any, Any]"],
|
2014
|
+
**kwargs: Any,
|
2015
|
+
):
|
2016
|
+
super().__init__(*args, **kwargs)
|
2017
|
+
self.work_pool = work_pool
|
2018
|
+
self.job_variables = job_variables
|
2019
|
+
self.worker_cls = worker_cls
|
2020
|
+
|
2021
|
+
@overload
|
2022
|
+
def __call__(self: "Flow[P, NoReturn]", *args: P.args, **kwargs: P.kwargs) -> None:
|
2023
|
+
# `NoReturn` matches if a type can't be inferred for the function which stops a
|
2024
|
+
# sync function from matching the `Coroutine` overload
|
2025
|
+
...
|
2026
|
+
|
2027
|
+
@overload
|
2028
|
+
def __call__(
|
2029
|
+
self: "Flow[P, Coroutine[Any, Any, T]]",
|
2030
|
+
*args: P.args,
|
2031
|
+
**kwargs: P.kwargs,
|
2032
|
+
) -> Coroutine[Any, Any, T]: ...
|
2033
|
+
|
2034
|
+
@overload
|
2035
|
+
def __call__(
|
2036
|
+
self: "Flow[P, T]",
|
2037
|
+
*args: P.args,
|
2038
|
+
**kwargs: P.kwargs,
|
2039
|
+
) -> T: ...
|
2040
|
+
|
2041
|
+
@overload
|
2042
|
+
def __call__(
|
2043
|
+
self: "Flow[P, Coroutine[Any, Any, T]]",
|
2044
|
+
*args: P.args,
|
2045
|
+
return_state: Literal[True],
|
2046
|
+
**kwargs: P.kwargs,
|
2047
|
+
) -> Awaitable[State[T]]: ...
|
2048
|
+
|
2049
|
+
@overload
|
2050
|
+
def __call__(
|
2051
|
+
self: "Flow[P, T]",
|
2052
|
+
*args: P.args,
|
2053
|
+
return_state: Literal[True],
|
2054
|
+
**kwargs: P.kwargs,
|
2055
|
+
) -> State[T]: ...
|
2056
|
+
|
2057
|
+
def __call__(
|
2058
|
+
self,
|
2059
|
+
*args: "P.args",
|
2060
|
+
return_state: bool = False,
|
2061
|
+
wait_for: Optional[Iterable[PrefectFuture[Any]]] = None,
|
2062
|
+
**kwargs: "P.kwargs",
|
2063
|
+
):
|
2064
|
+
async def modified_call(
|
2065
|
+
*args: P.args,
|
2066
|
+
return_state: bool = False,
|
2067
|
+
# TODO: Handle wait_for once we have an asynchronous way to wait for futures
|
2068
|
+
# We should wait locally for futures to resolve before spinning up
|
2069
|
+
# infrastructure.
|
2070
|
+
wait_for: Optional[Iterable[PrefectFuture[Any]]] = None,
|
2071
|
+
**kwargs: P.kwargs,
|
2072
|
+
) -> R | State[R]:
|
2073
|
+
try:
|
2074
|
+
async with self.worker_cls(work_pool_name=self.work_pool) as worker:
|
2075
|
+
parameters = get_call_parameters(self, args, kwargs)
|
2076
|
+
future = await worker.submit(
|
2077
|
+
flow=self,
|
2078
|
+
parameters=parameters,
|
2079
|
+
job_variables=self.job_variables,
|
2080
|
+
)
|
2081
|
+
if return_state:
|
2082
|
+
await future.wait_async()
|
2083
|
+
return future.state
|
2084
|
+
return await future.aresult()
|
2085
|
+
except (ExceptionGroup, BaseExceptionGroup) as exc:
|
2086
|
+
# For less verbose tracebacks
|
2087
|
+
exceptions = exc.exceptions
|
2088
|
+
if len(exceptions) == 1:
|
2089
|
+
raise exceptions[0] from None
|
2090
|
+
else:
|
2091
|
+
raise
|
2092
|
+
|
2093
|
+
if inspect.iscoroutinefunction(self.fn):
|
2094
|
+
return modified_call(
|
2095
|
+
*args, return_state=return_state, wait_for=wait_for, **kwargs
|
2096
|
+
)
|
2097
|
+
else:
|
2098
|
+
return run_coro_as_sync(
|
2099
|
+
modified_call(
|
2100
|
+
*args,
|
2101
|
+
return_state=return_state,
|
2102
|
+
wait_for=wait_for,
|
2103
|
+
**kwargs,
|
2104
|
+
)
|
2105
|
+
)
|
2106
|
+
|
2107
|
+
def submit(self, *args: P.args, **kwargs: P.kwargs) -> PrefectFlowRunFuture[R]:
|
2108
|
+
"""
|
2109
|
+
EXPERIMENTAL: This method is experimental and may be removed or changed in future
|
2110
|
+
releases.
|
2111
|
+
|
2112
|
+
Submit the flow to run on remote infrastructure.
|
2113
|
+
|
2114
|
+
Args:
|
2115
|
+
*args: Positional arguments to pass to the flow.
|
2116
|
+
**kwargs: Keyword arguments to pass to the flow.
|
2117
|
+
|
2118
|
+
Returns:
|
2119
|
+
A `PrefectFlowRunFuture` that can be used to retrieve the result of the flow run.
|
2120
|
+
|
2121
|
+
Examples:
|
2122
|
+
Submit a flow to run on Kubernetes:
|
2123
|
+
|
2124
|
+
```python
|
2125
|
+
from prefect import flow
|
2126
|
+
from prefect_kubernetes.experimental import kubernetes
|
2127
|
+
|
2128
|
+
@kubernetes(work_pool="my-kubernetes-work-pool")
|
2129
|
+
@flow
|
2130
|
+
def my_flow(x: int, y: int):
|
2131
|
+
return x + y
|
2132
|
+
|
2133
|
+
future = my_flow.submit(x=1, y=2)
|
2134
|
+
result = future.result()
|
2135
|
+
print(result)
|
2136
|
+
```
|
2137
|
+
"""
|
2138
|
+
|
2139
|
+
async def submit_func():
|
2140
|
+
async with self.worker_cls(work_pool_name=self.work_pool) as worker:
|
2141
|
+
parameters = get_call_parameters(self, args, kwargs)
|
2142
|
+
return await worker.submit(
|
2143
|
+
flow=self,
|
2144
|
+
parameters=parameters,
|
2145
|
+
job_variables=self.job_variables,
|
2146
|
+
)
|
2147
|
+
|
2148
|
+
return run_coro_as_sync(submit_func())
|
2149
|
+
|
2150
|
+
def with_options(
|
2151
|
+
self,
|
2152
|
+
*,
|
2153
|
+
name: Optional[str] = None,
|
2154
|
+
version: Optional[str] = None,
|
2155
|
+
retries: Optional[int] = None,
|
2156
|
+
retry_delay_seconds: Optional[Union[int, float]] = None,
|
2157
|
+
description: Optional[str] = None,
|
2158
|
+
flow_run_name: Optional[Union[Callable[[], str], str]] = None,
|
2159
|
+
task_runner: Union[
|
2160
|
+
Type[TaskRunner[PrefectFuture[Any]]], TaskRunner[PrefectFuture[Any]], None
|
2161
|
+
] = None,
|
2162
|
+
timeout_seconds: Union[int, float, None] = None,
|
2163
|
+
validate_parameters: Optional[bool] = None,
|
2164
|
+
persist_result: Optional[bool] = NotSet, # type: ignore
|
2165
|
+
result_storage: Optional[ResultStorage] = NotSet, # type: ignore
|
2166
|
+
result_serializer: Optional[ResultSerializer] = NotSet, # type: ignore
|
2167
|
+
cache_result_in_memory: Optional[bool] = None,
|
2168
|
+
log_prints: Optional[bool] = NotSet, # type: ignore
|
2169
|
+
on_completion: Optional[list[FlowStateHook[P, R]]] = None,
|
2170
|
+
on_failure: Optional[list[FlowStateHook[P, R]]] = None,
|
2171
|
+
on_cancellation: Optional[list[FlowStateHook[P, R]]] = None,
|
2172
|
+
on_crashed: Optional[list[FlowStateHook[P, R]]] = None,
|
2173
|
+
on_running: Optional[list[FlowStateHook[P, R]]] = None,
|
2174
|
+
job_variables: Optional[dict[str, Any]] = None,
|
2175
|
+
) -> "InfrastructureBoundFlow[P, R]":
|
2176
|
+
new_flow = super().with_options(
|
2177
|
+
name=name,
|
2178
|
+
version=version,
|
2179
|
+
retries=retries,
|
2180
|
+
retry_delay_seconds=retry_delay_seconds,
|
2181
|
+
description=description,
|
2182
|
+
flow_run_name=flow_run_name,
|
2183
|
+
task_runner=task_runner,
|
2184
|
+
timeout_seconds=timeout_seconds,
|
2185
|
+
validate_parameters=validate_parameters,
|
2186
|
+
persist_result=persist_result,
|
2187
|
+
result_storage=result_storage,
|
2188
|
+
result_serializer=result_serializer,
|
2189
|
+
cache_result_in_memory=cache_result_in_memory,
|
2190
|
+
log_prints=log_prints,
|
2191
|
+
on_completion=on_completion,
|
2192
|
+
on_failure=on_failure,
|
2193
|
+
on_cancellation=on_cancellation,
|
2194
|
+
on_crashed=on_crashed,
|
2195
|
+
on_running=on_running,
|
2196
|
+
)
|
2197
|
+
new_infrastructure_bound_flow = bind_flow_to_infrastructure(
|
2198
|
+
new_flow,
|
2199
|
+
self.work_pool,
|
2200
|
+
self.worker_cls,
|
2201
|
+
job_variables=job_variables
|
2202
|
+
if job_variables is not None
|
2203
|
+
else self.job_variables,
|
2204
|
+
)
|
2205
|
+
return new_infrastructure_bound_flow
|
2206
|
+
|
2207
|
+
|
2208
|
+
def bind_flow_to_infrastructure(
|
2209
|
+
flow: Flow[P, R],
|
2210
|
+
work_pool: str,
|
2211
|
+
worker_cls: type["BaseWorker[Any, Any, Any]"],
|
2212
|
+
job_variables: dict[str, Any] | None = None,
|
2213
|
+
) -> InfrastructureBoundFlow[P, R]:
|
2214
|
+
new = InfrastructureBoundFlow[P, R](
|
2215
|
+
flow.fn,
|
2216
|
+
work_pool=work_pool,
|
2217
|
+
job_variables=job_variables or {},
|
2218
|
+
worker_cls=worker_cls,
|
2219
|
+
)
|
2220
|
+
# Copy all attributes from the original flow
|
2221
|
+
for attr, value in flow.__dict__.items():
|
2222
|
+
setattr(new, attr, value)
|
2223
|
+
return new
|
2224
|
+
|
2225
|
+
|
1987
2226
|
def _raise_on_name_with_banned_characters(name: Optional[str]) -> Optional[str]:
|
1988
2227
|
"""
|
1989
2228
|
Raise an InvalidNameError if the given name contains any invalid
|
prefect/futures.py
CHANGED
@@ -612,19 +612,44 @@ def resolve_futures_to_states(
|
|
612
612
|
|
613
613
|
Unsupported object types will be returned without modification.
|
614
614
|
"""
|
615
|
-
futures: set[PrefectFuture[R]] = set()
|
616
615
|
|
617
|
-
def
|
618
|
-
|
619
|
-
|
620
|
-
# Expressions inside quotes should not be traversed
|
621
|
-
if isinstance(context.get("annotation"), quote):
|
622
|
-
raise StopVisiting()
|
616
|
+
def _resolve_state(future: PrefectFuture[R]):
|
617
|
+
future.wait()
|
618
|
+
return future.state
|
623
619
|
|
624
|
-
|
625
|
-
|
620
|
+
return _resolve_futures(
|
621
|
+
expr,
|
622
|
+
resolve_fn=_resolve_state,
|
623
|
+
)
|
626
624
|
|
627
|
-
|
625
|
+
|
626
|
+
def resolve_futures_to_results(
|
627
|
+
expr: PrefectFuture[R] | Any,
|
628
|
+
) -> Any:
|
629
|
+
"""
|
630
|
+
Given a Python built-in collection, recursively find `PrefectFutures` and build a
|
631
|
+
new collection with the same structure with futures resolved to their final results.
|
632
|
+
Resolving futures to their final result may wait for execution to complete.
|
633
|
+
|
634
|
+
Unsupported object types will be returned without modification.
|
635
|
+
"""
|
636
|
+
|
637
|
+
def _resolve_result(future: PrefectFuture[R]) -> Any:
|
638
|
+
future.wait()
|
639
|
+
if future.state.is_completed():
|
640
|
+
return future.result()
|
641
|
+
else:
|
642
|
+
raise Exception("At least one result did not complete successfully")
|
643
|
+
|
644
|
+
return _resolve_futures(expr, resolve_fn=_resolve_result)
|
645
|
+
|
646
|
+
|
647
|
+
def _resolve_futures(
|
648
|
+
expr: PrefectFuture[R] | Any,
|
649
|
+
resolve_fn: Callable[[PrefectFuture[R]], Any],
|
650
|
+
) -> Any:
|
651
|
+
"""Helper function to resolve PrefectFutures in a collection."""
|
652
|
+
futures: set[PrefectFuture[R]] = set()
|
628
653
|
|
629
654
|
visit_collection(
|
630
655
|
expr,
|
@@ -633,31 +658,39 @@ def resolve_futures_to_states(
|
|
633
658
|
context={},
|
634
659
|
)
|
635
660
|
|
636
|
-
#
|
661
|
+
# If no futures were found, return the original expression
|
637
662
|
if not futures:
|
638
663
|
return expr
|
639
664
|
|
640
|
-
#
|
641
|
-
|
642
|
-
for future in futures:
|
643
|
-
future.wait()
|
644
|
-
states.append(future.state)
|
645
|
-
|
646
|
-
states_by_future = dict(zip(futures, states))
|
665
|
+
# Resolve each future using the provided resolve function
|
666
|
+
resolved_values = {future: resolve_fn(future) for future in futures}
|
647
667
|
|
648
|
-
def
|
668
|
+
def replace_futures(expr: Any, context: Any) -> Any:
|
649
669
|
# Expressions inside quotes should not be modified
|
650
670
|
if isinstance(context.get("annotation"), quote):
|
651
671
|
raise StopVisiting()
|
652
672
|
|
653
673
|
if isinstance(expr, PrefectFuture):
|
654
|
-
return
|
674
|
+
return resolved_values[expr]
|
655
675
|
else:
|
656
676
|
return expr
|
657
677
|
|
658
678
|
return visit_collection(
|
659
679
|
expr,
|
660
|
-
visit_fn=
|
680
|
+
visit_fn=replace_futures,
|
661
681
|
return_data=True,
|
662
682
|
context={},
|
663
683
|
)
|
684
|
+
|
685
|
+
|
686
|
+
def _collect_futures(
|
687
|
+
futures: set[PrefectFuture[R]], expr: Any | PrefectFuture[R], context: Any
|
688
|
+
) -> Any | PrefectFuture[R]:
|
689
|
+
# Expressions inside quotes should not be traversed
|
690
|
+
if isinstance(context.get("annotation"), quote):
|
691
|
+
raise StopVisiting()
|
692
|
+
|
693
|
+
if isinstance(expr, PrefectFuture):
|
694
|
+
futures.add(expr)
|
695
|
+
|
696
|
+
return expr
|
prefect/runner/runner.py
CHANGED
@@ -636,7 +636,12 @@ class Runner:
|
|
636
636
|
|
637
637
|
return process
|
638
638
|
|
639
|
-
async def execute_bundle(
|
639
|
+
async def execute_bundle(
|
640
|
+
self,
|
641
|
+
bundle: SerializedBundle,
|
642
|
+
cwd: Path | str | None = None,
|
643
|
+
env: dict[str, str | None] | None = None,
|
644
|
+
) -> None:
|
640
645
|
"""
|
641
646
|
Executes a bundle in a subprocess.
|
642
647
|
"""
|
@@ -651,7 +656,7 @@ class Runner:
|
|
651
656
|
if not self._acquire_limit_slot(flow_run.id):
|
652
657
|
return
|
653
658
|
|
654
|
-
process = execute_bundle_in_subprocess(bundle)
|
659
|
+
process = execute_bundle_in_subprocess(bundle, cwd=cwd, env=env)
|
655
660
|
|
656
661
|
if process.pid is None:
|
657
662
|
# This shouldn't happen because `execute_bundle_in_subprocess` starts the process
|
@@ -776,7 +781,7 @@ class Runner:
|
|
776
781
|
if command is None:
|
777
782
|
runner_command = [get_sys_executable(), "-m", "prefect.engine"]
|
778
783
|
else:
|
779
|
-
runner_command = shlex.split(command)
|
784
|
+
runner_command = shlex.split(command, posix=(os.name != "nt"))
|
780
785
|
|
781
786
|
flow_run_logger = self._get_flow_run_logger(flow_run)
|
782
787
|
|
@@ -98,9 +98,31 @@ async def create_deployment(
|
|
98
98
|
)
|
99
99
|
|
100
100
|
# hydrate the input model into a full model
|
101
|
-
deployment_dict = deployment.model_dump(
|
102
|
-
exclude={"work_pool_name"},
|
101
|
+
deployment_dict: dict = deployment.model_dump(
|
102
|
+
exclude={"work_pool_name"},
|
103
|
+
exclude_unset=True,
|
103
104
|
)
|
105
|
+
|
106
|
+
requested_concurrency_limit = deployment_dict.pop(
|
107
|
+
"global_concurrency_limit_id", "unset"
|
108
|
+
)
|
109
|
+
if requested_concurrency_limit != "unset":
|
110
|
+
if requested_concurrency_limit:
|
111
|
+
concurrency_limit = (
|
112
|
+
await models.concurrency_limits_v2.read_concurrency_limit(
|
113
|
+
session=session,
|
114
|
+
concurrency_limit_id=requested_concurrency_limit,
|
115
|
+
)
|
116
|
+
)
|
117
|
+
|
118
|
+
if not concurrency_limit:
|
119
|
+
raise HTTPException(
|
120
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
121
|
+
detail="Concurrency limit not found",
|
122
|
+
)
|
123
|
+
|
124
|
+
deployment_dict["concurrency_limit_id"] = requested_concurrency_limit
|
125
|
+
|
104
126
|
if deployment.work_pool_name and deployment.work_queue_name:
|
105
127
|
# If a specific pool name/queue name combination was provided, get the
|
106
128
|
# ID for that work pool queue.
|
@@ -300,8 +322,24 @@ async def update_deployment(
|
|
300
322
|
detail="Invalid schema: Unable to validate schema with circular references.",
|
301
323
|
)
|
302
324
|
|
325
|
+
if deployment.global_concurrency_limit_id:
|
326
|
+
concurrency_limit = (
|
327
|
+
await models.concurrency_limits_v2.read_concurrency_limit(
|
328
|
+
session=session,
|
329
|
+
concurrency_limit_id=deployment.global_concurrency_limit_id,
|
330
|
+
)
|
331
|
+
)
|
332
|
+
|
333
|
+
if not concurrency_limit:
|
334
|
+
raise HTTPException(
|
335
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
336
|
+
detail="Concurrency limit not found",
|
337
|
+
)
|
338
|
+
|
303
339
|
result = await models.deployments.update_deployment(
|
304
|
-
session=session,
|
340
|
+
session=session,
|
341
|
+
deployment_id=deployment_id,
|
342
|
+
deployment=deployment,
|
305
343
|
)
|
306
344
|
|
307
345
|
for schedule in schedules_to_patch:
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import ast
|
2
|
+
import math
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
4
|
+
|
5
|
+
import anyio
|
6
|
+
from typing_extensions import TypeAlias
|
7
|
+
|
8
|
+
from prefect.logging.loggers import get_logger
|
9
|
+
from prefect.settings import get_current_settings
|
10
|
+
from prefect.utilities.asyncutils import LazySemaphore
|
11
|
+
from prefect.utilities.filesystem import get_open_file_limit
|
12
|
+
|
13
|
+
# Only allow half of the open file limit to be open at once to allow for other
|
14
|
+
# actors to open files.
|
15
|
+
OPEN_FILE_SEMAPHORE = LazySemaphore(lambda: math.floor(get_open_file_limit() * 0.5))
|
16
|
+
|
17
|
+
# this potentially could be a TypedDict, but you
|
18
|
+
# need some way to convince the type checker that
|
19
|
+
# Literal["flow_name", "task_name"] are being provided
|
20
|
+
DecoratedFnMetadata: TypeAlias = dict[str, str]
|
21
|
+
|
22
|
+
|
23
|
+
async def find_prefect_decorated_functions_in_file(
|
24
|
+
path: anyio.Path, decorator_module: str, decorator_name: Literal["flow", "task"]
|
25
|
+
) -> list[DecoratedFnMetadata]:
|
26
|
+
logger = get_logger()
|
27
|
+
decorator_name_key = f"{decorator_name}_name"
|
28
|
+
decorated_functions: list[DecoratedFnMetadata] = []
|
29
|
+
|
30
|
+
async with OPEN_FILE_SEMAPHORE:
|
31
|
+
try:
|
32
|
+
async with await anyio.open_file(path) as f:
|
33
|
+
try:
|
34
|
+
tree = ast.parse(await f.read())
|
35
|
+
except SyntaxError:
|
36
|
+
if get_current_settings().debug_mode:
|
37
|
+
logger.debug(
|
38
|
+
f"Could not parse {path} as a Python file. Skipping."
|
39
|
+
)
|
40
|
+
return decorated_functions
|
41
|
+
except Exception as exc:
|
42
|
+
if get_current_settings().debug_mode:
|
43
|
+
logger.debug(f"Could not open {path}: {exc}. Skipping.")
|
44
|
+
return decorated_functions
|
45
|
+
|
46
|
+
for node in ast.walk(tree):
|
47
|
+
if isinstance(
|
48
|
+
node,
|
49
|
+
(
|
50
|
+
ast.FunctionDef,
|
51
|
+
ast.AsyncFunctionDef,
|
52
|
+
),
|
53
|
+
):
|
54
|
+
for decorator in node.decorator_list:
|
55
|
+
# handles @decorator_name
|
56
|
+
is_name_match = (
|
57
|
+
isinstance(decorator, ast.Name) and decorator.id == decorator_name
|
58
|
+
)
|
59
|
+
# handles @decorator_name()
|
60
|
+
is_func_name_match = (
|
61
|
+
isinstance(decorator, ast.Call)
|
62
|
+
and isinstance(decorator.func, ast.Name)
|
63
|
+
and decorator.func.id == decorator_name
|
64
|
+
)
|
65
|
+
# handles @decorator_module.decorator_name
|
66
|
+
is_module_attribute_match = (
|
67
|
+
isinstance(decorator, ast.Attribute)
|
68
|
+
and isinstance(decorator.value, ast.Name)
|
69
|
+
and decorator.value.id == decorator_module
|
70
|
+
and decorator.attr == decorator_name
|
71
|
+
)
|
72
|
+
# handles @decorator_module.decorator_name()
|
73
|
+
is_module_attribute_func_match = (
|
74
|
+
isinstance(decorator, ast.Call)
|
75
|
+
and isinstance(decorator.func, ast.Attribute)
|
76
|
+
and decorator.func.attr == decorator_name
|
77
|
+
and isinstance(decorator.func.value, ast.Name)
|
78
|
+
and decorator.func.value.id == decorator_module
|
79
|
+
)
|
80
|
+
if is_name_match or is_module_attribute_match:
|
81
|
+
decorated_functions.append(
|
82
|
+
{
|
83
|
+
decorator_name_key: node.name,
|
84
|
+
"function_name": node.name,
|
85
|
+
"filepath": str(path),
|
86
|
+
}
|
87
|
+
)
|
88
|
+
if is_func_name_match or is_module_attribute_func_match:
|
89
|
+
name_kwarg_node = None
|
90
|
+
if TYPE_CHECKING:
|
91
|
+
assert isinstance(decorator, ast.Call)
|
92
|
+
for kw in decorator.keywords:
|
93
|
+
if kw.arg == "name":
|
94
|
+
name_kwarg_node = kw
|
95
|
+
break
|
96
|
+
if name_kwarg_node is not None and isinstance(
|
97
|
+
name_kwarg_node.value, ast.Constant
|
98
|
+
):
|
99
|
+
decorated_fn_name = name_kwarg_node.value.value
|
100
|
+
else:
|
101
|
+
decorated_fn_name = node.name
|
102
|
+
decorated_functions.append(
|
103
|
+
{
|
104
|
+
decorator_name_key: decorated_fn_name,
|
105
|
+
"function_name": node.name,
|
106
|
+
"filepath": str(path),
|
107
|
+
}
|
108
|
+
)
|
109
|
+
return decorated_functions
|
110
|
+
|
111
|
+
|
112
|
+
async def find_flow_functions_in_file(path: anyio.Path) -> list[DecoratedFnMetadata]:
|
113
|
+
return await find_prefect_decorated_functions_in_file(path, "prefect", "flow")
|
114
|
+
|
115
|
+
|
116
|
+
async def find_task_functions_in_file(path: anyio.Path) -> list[DecoratedFnMetadata]:
|
117
|
+
return await find_prefect_decorated_functions_in_file(path, "prefect", "task")
|