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
@@ -151,7 +151,7 @@ class from_async(_base):
|
|
151
151
|
__call: Union[Callable[[], T], Call[T]],
|
152
152
|
timeout: Optional[float] = None,
|
153
153
|
done_callbacks: Optional[Iterable[Call]] = None,
|
154
|
-
) ->
|
154
|
+
) -> T:
|
155
155
|
call = _cast_to_call(__call)
|
156
156
|
waiter = AsyncWaiter(call=call)
|
157
157
|
for callback in done_callbacks or []:
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import asyncio
|
2
|
+
from functools import wraps
|
3
|
+
from typing import Any, Callable, Tuple, Type
|
4
|
+
|
5
|
+
from prefect.logging.loggers import get_logger
|
6
|
+
from prefect.utilities.math import clamped_poisson_interval
|
7
|
+
|
8
|
+
logger = get_logger("retries")
|
9
|
+
|
10
|
+
|
11
|
+
def exponential_backoff_with_jitter(
|
12
|
+
attempt: int, base_delay: float, max_delay: float
|
13
|
+
) -> float:
|
14
|
+
average_interval = min(base_delay * (2**attempt), max_delay)
|
15
|
+
return clamped_poisson_interval(average_interval, clamping_factor=0.3)
|
16
|
+
|
17
|
+
|
18
|
+
def retry_async_fn(
|
19
|
+
max_attempts: int = 3,
|
20
|
+
backoff_strategy: Callable[
|
21
|
+
[int, float, float], float
|
22
|
+
] = exponential_backoff_with_jitter,
|
23
|
+
base_delay: float = 1,
|
24
|
+
max_delay: float = 10,
|
25
|
+
retry_on_exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
26
|
+
):
|
27
|
+
"""A decorator for retrying an async function.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
max_attempts: The maximum number of times to retry the function.
|
31
|
+
backoff_strategy: A function that takes in the number of attempts, the base
|
32
|
+
delay, and the maximum delay, and returns the delay to use for the next
|
33
|
+
attempt. Defaults to an exponential backoff with jitter.
|
34
|
+
base_delay: The base delay to use for the first attempt.
|
35
|
+
max_delay: The maximum delay to use for the last attempt.
|
36
|
+
retry_on_exceptions: A tuple of exception types to retry on. Defaults to
|
37
|
+
retrying on all exceptions.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def decorator(func):
|
41
|
+
@wraps(func)
|
42
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
43
|
+
for attempt in range(max_attempts):
|
44
|
+
try:
|
45
|
+
return await func(*args, **kwargs)
|
46
|
+
except retry_on_exceptions as e:
|
47
|
+
if attempt == max_attempts - 1:
|
48
|
+
logger.exception(
|
49
|
+
f"Function {func.__name__!r} failed after {max_attempts} attempts"
|
50
|
+
)
|
51
|
+
raise
|
52
|
+
delay = backoff_strategy(attempt, base_delay, max_delay)
|
53
|
+
logger.warning(
|
54
|
+
f"Attempt {attempt + 1} of function {func.__name__!r} failed with {type(e).__name__}. "
|
55
|
+
f"Retrying in {delay:.2f} seconds..."
|
56
|
+
)
|
57
|
+
await asyncio.sleep(delay)
|
58
|
+
|
59
|
+
return wrapper
|
60
|
+
|
61
|
+
return decorator
|
prefect/client/cloud.py
CHANGED
@@ -9,7 +9,7 @@ from starlette import status
|
|
9
9
|
import prefect.context
|
10
10
|
import prefect.settings
|
11
11
|
from prefect.client.base import PrefectHttpxAsyncClient
|
12
|
-
from prefect.client.schemas import Workspace
|
12
|
+
from prefect.client.schemas.objects import Workspace
|
13
13
|
from prefect.exceptions import ObjectNotFound, PrefectException
|
14
14
|
from prefect.settings import (
|
15
15
|
PREFECT_API_KEY,
|
prefect/concurrency/asyncio.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import asyncio
|
2
2
|
from contextlib import asynccontextmanager
|
3
|
-
from typing import List, Literal, Optional, Union, cast
|
3
|
+
from typing import AsyncGenerator, List, Literal, Optional, Union, cast
|
4
4
|
|
5
5
|
import httpx
|
6
6
|
import pendulum
|
@@ -34,7 +34,7 @@ async def concurrency(
|
|
34
34
|
names: Union[str, List[str]],
|
35
35
|
occupy: int = 1,
|
36
36
|
timeout_seconds: Optional[float] = None,
|
37
|
-
):
|
37
|
+
) -> AsyncGenerator[None, None]:
|
38
38
|
"""A context manager that acquires and releases concurrency slots from the
|
39
39
|
given concurrency limits.
|
40
40
|
|
@@ -77,7 +77,7 @@ async def concurrency(
|
|
77
77
|
_emit_concurrency_release_events(limits, occupy, emitted_events)
|
78
78
|
|
79
79
|
|
80
|
-
async def rate_limit(names: Union[str, List[str]], occupy: int = 1):
|
80
|
+
async def rate_limit(names: Union[str, List[str]], occupy: int = 1) -> None:
|
81
81
|
"""Block execution until an `occupy` number of slots of the concurrency
|
82
82
|
limits given in `names` are acquired. Requires that all given concurrency
|
83
83
|
limits have a slot decay.
|
prefect/concurrency/events.py
CHANGED
@@ -54,6 +54,6 @@ def _emit_concurrency_release_events(
|
|
54
54
|
limits: List[MinimalConcurrencyLimitResponse],
|
55
55
|
occupy: int,
|
56
56
|
events: Dict[UUID, Optional[Event]],
|
57
|
-
):
|
57
|
+
) -> None:
|
58
58
|
for limit in limits:
|
59
59
|
_emit_concurrency_event("released", limit, limits, occupy, events[limit.id])
|
prefect/concurrency/services.py
CHANGED
@@ -3,6 +3,7 @@ import concurrent.futures
|
|
3
3
|
from contextlib import asynccontextmanager
|
4
4
|
from typing import (
|
5
5
|
TYPE_CHECKING,
|
6
|
+
AsyncGenerator,
|
6
7
|
FrozenSet,
|
7
8
|
Optional,
|
8
9
|
Tuple,
|
@@ -27,14 +28,14 @@ class ConcurrencySlotAcquisitionService(QueueService):
|
|
27
28
|
self.concurrency_limit_names = sorted(list(concurrency_limit_names))
|
28
29
|
|
29
30
|
@asynccontextmanager
|
30
|
-
async def _lifespan(self):
|
31
|
+
async def _lifespan(self) -> AsyncGenerator[None, None]:
|
31
32
|
async with get_client() as client:
|
32
33
|
self._client = client
|
33
34
|
yield
|
34
35
|
|
35
36
|
async def _handle(
|
36
37
|
self, item: Tuple[int, str, Optional[float], concurrent.futures.Future]
|
37
|
-
):
|
38
|
+
) -> None:
|
38
39
|
occupy, mode, timeout_seconds, future = item
|
39
40
|
try:
|
40
41
|
response = await self.acquire_slots(occupy, mode, timeout_seconds)
|
prefect/concurrency/sync.py
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
from contextlib import contextmanager
|
2
|
-
from typing import
|
2
|
+
from typing import (
|
3
|
+
Any,
|
4
|
+
Awaitable,
|
5
|
+
Callable,
|
6
|
+
Generator,
|
7
|
+
List,
|
8
|
+
Optional,
|
9
|
+
TypeVar,
|
10
|
+
Union,
|
11
|
+
cast,
|
12
|
+
)
|
3
13
|
|
4
14
|
import pendulum
|
5
15
|
|
@@ -22,13 +32,15 @@ from .events import (
|
|
22
32
|
_emit_concurrency_release_events,
|
23
33
|
)
|
24
34
|
|
35
|
+
T = TypeVar("T")
|
36
|
+
|
25
37
|
|
26
38
|
@contextmanager
|
27
39
|
def concurrency(
|
28
40
|
names: Union[str, List[str]],
|
29
41
|
occupy: int = 1,
|
30
42
|
timeout_seconds: Optional[float] = None,
|
31
|
-
):
|
43
|
+
) -> Generator[None, None, None]:
|
32
44
|
"""A context manager that acquires and releases concurrency slots from the
|
33
45
|
given concurrency limits.
|
34
46
|
|
@@ -75,7 +87,7 @@ def concurrency(
|
|
75
87
|
_emit_concurrency_release_events(limits, occupy, emitted_events)
|
76
88
|
|
77
89
|
|
78
|
-
def rate_limit(names: Union[str, List[str]], occupy: int = 1):
|
90
|
+
def rate_limit(names: Union[str, List[str]], occupy: int = 1) -> None:
|
79
91
|
"""Block execution until an `occupy` number of slots of the concurrency
|
80
92
|
limits given in `names` are acquired. Requires that all given concurrency
|
81
93
|
limits have a slot decay.
|
@@ -91,11 +103,13 @@ def rate_limit(names: Union[str, List[str]], occupy: int = 1):
|
|
91
103
|
_emit_concurrency_acquisition_events(limits, occupy)
|
92
104
|
|
93
105
|
|
94
|
-
def _call_async_function_from_sync(
|
106
|
+
def _call_async_function_from_sync(
|
107
|
+
fn: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
|
108
|
+
) -> T:
|
95
109
|
loop = get_running_loop()
|
96
110
|
call = create_call(fn, *args, **kwargs)
|
97
111
|
|
98
112
|
if loop is not None:
|
99
113
|
return from_sync.call_soon_in_loop_thread(call).result()
|
100
114
|
else:
|
101
|
-
return call()
|
115
|
+
return call() # type: ignore [return-value]
|
prefect/context.py
CHANGED
@@ -9,6 +9,7 @@ For more user-accessible information about the current run, see [`prefect.runtim
|
|
9
9
|
import os
|
10
10
|
import sys
|
11
11
|
import warnings
|
12
|
+
import weakref
|
12
13
|
from contextlib import ExitStack, contextmanager
|
13
14
|
from contextvars import ContextVar, Token
|
14
15
|
from pathlib import Path
|
@@ -17,6 +18,7 @@ from typing import (
|
|
17
18
|
Any,
|
18
19
|
Dict,
|
19
20
|
Generator,
|
21
|
+
Mapping,
|
20
22
|
Optional,
|
21
23
|
Set,
|
22
24
|
Type,
|
@@ -291,8 +293,12 @@ class EngineContext(RunContext):
|
|
291
293
|
# Counter for flow pauses
|
292
294
|
observed_flow_pauses: Dict[str, int] = Field(default_factory=dict)
|
293
295
|
|
294
|
-
# Tracking for result from task runs in this flow run
|
295
|
-
|
296
|
+
# Tracking for result from task runs in this flow run for dependency tracking
|
297
|
+
# Holds the ID of the object returned by the task run and task run state
|
298
|
+
# This is a weakref dictionary to avoid undermining garbage collection
|
299
|
+
task_run_results: Mapping[int, State] = Field(
|
300
|
+
default_factory=weakref.WeakValueDictionary
|
301
|
+
)
|
296
302
|
|
297
303
|
# Events worker to emit events to Prefect Cloud
|
298
304
|
events: Optional[EventsWorker] = None
|
prefect/deployments/__init__.py
CHANGED
@@ -1,20 +1,33 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
1
2
|
from prefect._internal.compatibility.migration import getattr_migration
|
2
|
-
import prefect.deployments.base
|
3
|
-
import prefect.deployments.steps
|
4
|
-
from prefect.deployments.base import (
|
5
|
-
initialize_project,
|
6
|
-
)
|
7
3
|
|
8
|
-
from prefect.deployments.runner import (
|
9
|
-
RunnerDeployment,
|
10
|
-
deploy,
|
11
|
-
DockerImage,
|
12
|
-
EntrypointType,
|
13
|
-
)
|
14
4
|
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from .flow_runs import run_deployment
|
7
|
+
from .base import initialize_project
|
8
|
+
from .runner import deploy
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
)
|
10
|
+
_public_api: dict[str, tuple[str, str]] = {
|
11
|
+
"initialize_project": (__spec__.parent, ".base"),
|
12
|
+
"run_deployment": (__spec__.parent, ".flow_runs"),
|
13
|
+
"deploy": (__spec__.parent, ".runner"),
|
14
|
+
}
|
19
15
|
|
20
|
-
|
16
|
+
# Declare API for type-checkers
|
17
|
+
__all__ = ["initialize_project", "deploy", "run_deployment"]
|
18
|
+
|
19
|
+
|
20
|
+
def __getattr__(attr_name: str) -> object:
|
21
|
+
dynamic_attr = _public_api.get(attr_name)
|
22
|
+
if dynamic_attr is None:
|
23
|
+
return getattr_migration(__name__)(attr_name)
|
24
|
+
|
25
|
+
package, module_name = dynamic_attr
|
26
|
+
|
27
|
+
from importlib import import_module
|
28
|
+
|
29
|
+
if module_name == "__module__":
|
30
|
+
return import_module(f".{attr_name}", package=package)
|
31
|
+
else:
|
32
|
+
module = import_module(module_name, package=package)
|
33
|
+
return getattr(module, attr_name)
|
@@ -6,6 +6,7 @@ import os
|
|
6
6
|
from pathlib import Path
|
7
7
|
from typing import TYPE_CHECKING, Any, Optional
|
8
8
|
|
9
|
+
from prefect._internal.retries import retry_async_fn
|
9
10
|
from prefect.logging.loggers import get_logger
|
10
11
|
from prefect.runner.storage import BlockStorageAdapter, GitRepository, RemoteStorage
|
11
12
|
from prefect.utilities.asyncutils import sync_compatible
|
@@ -31,6 +32,12 @@ def set_working_directory(directory: str) -> dict:
|
|
31
32
|
return dict(directory=directory)
|
32
33
|
|
33
34
|
|
35
|
+
@retry_async_fn(
|
36
|
+
max_attempts=3,
|
37
|
+
base_delay=1,
|
38
|
+
max_delay=10,
|
39
|
+
retry_on_exceptions=(RuntimeError,),
|
40
|
+
)
|
34
41
|
@sync_compatible
|
35
42
|
async def git_clone(
|
36
43
|
repository: str,
|
prefect/flow_engine.py
CHANGED
@@ -7,7 +7,6 @@ from dataclasses import dataclass, field
|
|
7
7
|
from typing import (
|
8
8
|
Any,
|
9
9
|
AsyncGenerator,
|
10
|
-
Callable,
|
11
10
|
Coroutine,
|
12
11
|
Dict,
|
13
12
|
Generator,
|
@@ -415,7 +414,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
415
414
|
|
416
415
|
return flow_run
|
417
416
|
|
418
|
-
def call_hooks(self, state: Optional[State] = None)
|
417
|
+
def call_hooks(self, state: Optional[State] = None):
|
419
418
|
if state is None:
|
420
419
|
state = self.state
|
421
420
|
flow = self.flow
|
@@ -613,11 +612,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
613
612
|
|
614
613
|
if self.state.is_running():
|
615
614
|
self.call_hooks()
|
616
|
-
|
617
|
-
yield
|
618
|
-
finally:
|
619
|
-
if self.state.is_final() or self.state.is_cancelling():
|
620
|
-
self.call_hooks()
|
615
|
+
yield
|
621
616
|
|
622
617
|
@contextmanager
|
623
618
|
def run_context(self):
|
@@ -638,6 +633,9 @@ class FlowRunEngine(Generic[P, R]):
|
|
638
633
|
except Exception as exc:
|
639
634
|
self.logger.exception("Encountered exception during execution: %r", exc)
|
640
635
|
self.handle_exception(exc)
|
636
|
+
finally:
|
637
|
+
if self.state.is_final() or self.state.is_cancelling():
|
638
|
+
self.call_hooks()
|
641
639
|
|
642
640
|
def call_flow_fn(self) -> Union[R, Coroutine[Any, Any, R]]:
|
643
641
|
"""
|