prefect-client 3.0.0rc4__py3-none-any.whl → 3.0.0rc5__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/__init__.py +0 -2
- prefect/client/schemas/schedules.py +9 -2
- prefect/client/types/__init__.py +0 -0
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/concurrency/asyncio.py +14 -4
- prefect/concurrency/services.py +29 -22
- prefect/concurrency/sync.py +3 -5
- prefect/context.py +0 -114
- prefect/deployments/__init__.py +1 -1
- prefect/deployments/runner.py +11 -93
- prefect/deployments/schedules.py +5 -7
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/flow_engine.py +14 -18
- prefect/flows.py +24 -93
- prefect/futures.py +13 -1
- prefect/infrastructure/provisioners/cloud_run.py +2 -2
- prefect/infrastructure/provisioners/container_instance.py +2 -2
- prefect/infrastructure/provisioners/ecs.py +2 -2
- prefect/records/result_store.py +5 -1
- prefect/results.py +78 -11
- prefect/runner/runner.py +5 -3
- prefect/runner/server.py +6 -2
- prefect/states.py +13 -3
- prefect/task_engine.py +2 -0
- prefect/tasks.py +0 -2
- prefect/transactions.py +2 -2
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/dockerutils.py +2 -1
- {prefect_client-3.0.0rc4.dist-info → prefect_client-3.0.0rc5.dist-info}/METADATA +1 -1
- {prefect_client-3.0.0rc4.dist-info → prefect_client-3.0.0rc5.dist-info}/RECORD +34 -29
- {prefect_client-3.0.0rc4.dist-info → prefect_client-3.0.0rc5.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc4.dist-info → prefect_client-3.0.0rc5.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc4.dist-info → prefect_client-3.0.0rc5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pendulum import now as pendulum_now
|
5
|
+
|
6
|
+
from prefect.settings import (
|
7
|
+
PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE,
|
8
|
+
)
|
9
|
+
from prefect.utilities.dockerutils import (
|
10
|
+
PushError,
|
11
|
+
build_image,
|
12
|
+
docker_client,
|
13
|
+
generate_default_dockerfile,
|
14
|
+
parse_image_tag,
|
15
|
+
split_repository_path,
|
16
|
+
)
|
17
|
+
from prefect.utilities.slugify import slugify
|
18
|
+
|
19
|
+
|
20
|
+
class DockerImage:
|
21
|
+
"""
|
22
|
+
Configuration used to build and push a Docker image for a deployment.
|
23
|
+
|
24
|
+
Attributes:
|
25
|
+
name: The name of the Docker image to build, including the registry and
|
26
|
+
repository.
|
27
|
+
tag: The tag to apply to the built image.
|
28
|
+
dockerfile: The path to the Dockerfile to use for building the image. If
|
29
|
+
not provided, a default Dockerfile will be generated.
|
30
|
+
**build_kwargs: Additional keyword arguments to pass to the Docker build request.
|
31
|
+
See the [`docker-py` documentation](https://docker-py.readthedocs.io/en/stable/images.html#docker.models.images.ImageCollection.build)
|
32
|
+
for more information.
|
33
|
+
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self, name: str, tag: Optional[str] = None, dockerfile="auto", **build_kwargs
|
38
|
+
):
|
39
|
+
image_name, image_tag = parse_image_tag(name)
|
40
|
+
if tag and image_tag:
|
41
|
+
raise ValueError(
|
42
|
+
f"Only one tag can be provided - both {image_tag!r} and {tag!r} were"
|
43
|
+
" provided as tags."
|
44
|
+
)
|
45
|
+
namespace, repository = split_repository_path(image_name)
|
46
|
+
# if the provided image name does not include a namespace (registry URL or user/org name),
|
47
|
+
# use the default namespace
|
48
|
+
if not namespace:
|
49
|
+
namespace = PREFECT_DEFAULT_DOCKER_BUILD_NAMESPACE.value()
|
50
|
+
# join the namespace and repository to create the full image name
|
51
|
+
# ignore namespace if it is None
|
52
|
+
self.name = "/".join(filter(None, [namespace, repository]))
|
53
|
+
self.tag = tag or image_tag or slugify(pendulum_now("utc").isoformat())
|
54
|
+
self.dockerfile = dockerfile
|
55
|
+
self.build_kwargs = build_kwargs
|
56
|
+
|
57
|
+
@property
|
58
|
+
def reference(self):
|
59
|
+
return f"{self.name}:{self.tag}"
|
60
|
+
|
61
|
+
def build(self):
|
62
|
+
full_image_name = self.reference
|
63
|
+
build_kwargs = self.build_kwargs.copy()
|
64
|
+
build_kwargs["context"] = Path.cwd()
|
65
|
+
build_kwargs["tag"] = full_image_name
|
66
|
+
build_kwargs["pull"] = build_kwargs.get("pull", True)
|
67
|
+
|
68
|
+
if self.dockerfile == "auto":
|
69
|
+
with generate_default_dockerfile():
|
70
|
+
build_image(**build_kwargs)
|
71
|
+
else:
|
72
|
+
build_kwargs["dockerfile"] = self.dockerfile
|
73
|
+
build_image(**build_kwargs)
|
74
|
+
|
75
|
+
def push(self):
|
76
|
+
with docker_client() as client:
|
77
|
+
events = client.api.push(
|
78
|
+
repository=self.name, tag=self.tag, stream=True, decode=True
|
79
|
+
)
|
80
|
+
for event in events:
|
81
|
+
if "error" in event:
|
82
|
+
raise PushError(event["error"])
|
prefect/flow_engine.py
CHANGED
@@ -168,6 +168,20 @@ class FlowRunEngine(Generic[P, R]):
|
|
168
168
|
)
|
169
169
|
return state
|
170
170
|
|
171
|
+
# validate prior to context so that context receives validated params
|
172
|
+
if self.flow.should_validate_parameters:
|
173
|
+
try:
|
174
|
+
self.parameters = self.flow.validate_parameters(self.parameters or {})
|
175
|
+
except Exception as exc:
|
176
|
+
message = "Validation of flow parameters failed with error:"
|
177
|
+
self.logger.error("%s %s", message, exc)
|
178
|
+
self.handle_exception(
|
179
|
+
exc,
|
180
|
+
msg=message,
|
181
|
+
result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
|
182
|
+
)
|
183
|
+
self.short_circuit = True
|
184
|
+
|
171
185
|
new_state = Running()
|
172
186
|
state = self.set_state(new_state)
|
173
187
|
while state.is_pending():
|
@@ -484,24 +498,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
484
498
|
flow_version=self.flow.version,
|
485
499
|
empirical_policy=self.flow_run.empirical_policy,
|
486
500
|
)
|
487
|
-
|
488
|
-
# validate prior to context so that context receives validated params
|
489
|
-
if self.flow.should_validate_parameters:
|
490
|
-
try:
|
491
|
-
self.parameters = self.flow.validate_parameters(
|
492
|
-
self.parameters or {}
|
493
|
-
)
|
494
|
-
except Exception as exc:
|
495
|
-
message = "Validation of flow parameters failed with error:"
|
496
|
-
self.logger.error("%s %s", message, exc)
|
497
|
-
self.handle_exception(
|
498
|
-
exc,
|
499
|
-
msg=message,
|
500
|
-
result_factory=run_coro_as_sync(
|
501
|
-
ResultFactory.from_flow(self.flow)
|
502
|
-
),
|
503
|
-
)
|
504
|
-
self.short_circuit = True
|
505
501
|
try:
|
506
502
|
yield self
|
507
503
|
except Exception:
|
prefect/flows.py
CHANGED
@@ -17,11 +17,9 @@ import warnings
|
|
17
17
|
from copy import copy
|
18
18
|
from functools import partial, update_wrapper
|
19
19
|
from pathlib import Path
|
20
|
-
from tempfile import NamedTemporaryFile
|
21
20
|
from typing import (
|
22
21
|
TYPE_CHECKING,
|
23
22
|
Any,
|
24
|
-
AnyStr,
|
25
23
|
Awaitable,
|
26
24
|
Callable,
|
27
25
|
Coroutine,
|
@@ -56,9 +54,9 @@ from prefect.client.schemas.objects import Flow as FlowSchema
|
|
56
54
|
from prefect.client.schemas.objects import FlowRun
|
57
55
|
from prefect.client.schemas.schedules import SCHEDULE_TYPES
|
58
56
|
from prefect.client.utilities import client_injector
|
59
|
-
from prefect.
|
60
|
-
from prefect.deployments.runner import DeploymentImage, EntrypointType, deploy
|
57
|
+
from prefect.deployments.runner import deploy
|
61
58
|
from prefect.deployments.steps.core import run_steps
|
59
|
+
from prefect.docker.docker_image import DockerImage
|
62
60
|
from prefect.events import DeploymentTriggerTypes, TriggerTypes
|
63
61
|
from prefect.exceptions import (
|
64
62
|
InvalidNameError,
|
@@ -87,6 +85,7 @@ from prefect.settings import (
|
|
87
85
|
from prefect.states import State
|
88
86
|
from prefect.task_runners import TaskRunner, ThreadPoolTaskRunner
|
89
87
|
from prefect.types import BANNED_CHARACTERS, WITHOUT_BANNED_CHARACTERS
|
88
|
+
from prefect.types.entrypoint import EntrypointType
|
90
89
|
from prefect.utilities.annotations import NotSet
|
91
90
|
from prefect.utilities.asyncutils import (
|
92
91
|
run_sync_in_worker_thread,
|
@@ -118,11 +117,11 @@ logger = get_logger("flows")
|
|
118
117
|
|
119
118
|
if TYPE_CHECKING:
|
120
119
|
from prefect.client.orchestration import PrefectClient
|
121
|
-
from prefect.
|
120
|
+
from prefect.client.types.flexible_schedule_list import FlexibleScheduleList
|
121
|
+
from prefect.deployments.runner import RunnerDeployment
|
122
122
|
from prefect.flows import FlowRun
|
123
123
|
|
124
124
|
|
125
|
-
@PrefectObjectRegistry.register_instances
|
126
125
|
class Flow(Generic[P, R]):
|
127
126
|
"""
|
128
127
|
A Prefect workflow definition.
|
@@ -145,7 +144,7 @@ class Flow(Generic[P, R]):
|
|
145
144
|
be provided as a string template with the flow's parameters as variables,
|
146
145
|
or a function that returns a string.
|
147
146
|
task_runner: An optional task runner to use for task execution within the flow;
|
148
|
-
if not provided, a `
|
147
|
+
if not provided, a `ThreadPoolTaskRunner` will be used.
|
149
148
|
description: An optional string description for the flow; if not provided, the
|
150
149
|
description will be pulled from the docstring for the decorated function.
|
151
150
|
timeout_seconds: An optional number of seconds indicating a maximum runtime for
|
@@ -998,7 +997,7 @@ class Flow(Generic[P, R]):
|
|
998
997
|
self,
|
999
998
|
name: str,
|
1000
999
|
work_pool_name: Optional[str] = None,
|
1001
|
-
image: Optional[Union[str,
|
1000
|
+
image: Optional[Union[str, DockerImage]] = None,
|
1002
1001
|
build: bool = True,
|
1003
1002
|
push: bool = True,
|
1004
1003
|
work_queue_name: Optional[str] = None,
|
@@ -1034,7 +1033,7 @@ class Flow(Generic[P, R]):
|
|
1034
1033
|
work_pool_name: The name of the work pool to use for this deployment. Defaults to
|
1035
1034
|
the value of `PREFECT_DEFAULT_WORK_POOL_NAME`.
|
1036
1035
|
image: The name of the Docker image to build, including the registry and
|
1037
|
-
repository. Pass a
|
1036
|
+
repository. Pass a DockerImage instance to customize the Dockerfile used
|
1038
1037
|
and build arguments.
|
1039
1038
|
build: Whether or not to build a new image for the flow. If False, the provided
|
1040
1039
|
image will be used as-is and pulled at runtime.
|
@@ -1638,47 +1637,6 @@ def select_flow(
|
|
1638
1637
|
return list(flows_dict.values())[0]
|
1639
1638
|
|
1640
1639
|
|
1641
|
-
def load_flows_from_script(path: str) -> List[Flow]:
|
1642
|
-
"""
|
1643
|
-
Load all flow objects from the given python script. All of the code in the file
|
1644
|
-
will be executed.
|
1645
|
-
|
1646
|
-
Returns:
|
1647
|
-
A list of flows
|
1648
|
-
|
1649
|
-
Raises:
|
1650
|
-
FlowScriptError: If an exception is encountered while running the script
|
1651
|
-
"""
|
1652
|
-
return registry_from_script(path).get_instances(Flow)
|
1653
|
-
|
1654
|
-
|
1655
|
-
def load_flow_from_script(path: str, flow_name: Optional[str] = None) -> Flow:
|
1656
|
-
"""
|
1657
|
-
Extract a flow object from a script by running all of the code in the file.
|
1658
|
-
|
1659
|
-
If the script has multiple flows in it, a flow name must be provided to specify
|
1660
|
-
the flow to return.
|
1661
|
-
|
1662
|
-
Args:
|
1663
|
-
path: A path to a Python script containing flows
|
1664
|
-
flow_name: An optional flow name to look for in the script
|
1665
|
-
|
1666
|
-
Returns:
|
1667
|
-
The flow object from the script
|
1668
|
-
|
1669
|
-
Raises:
|
1670
|
-
FlowScriptError: If an exception is encountered while running the script
|
1671
|
-
MissingFlowError: If no flows exist in the iterable
|
1672
|
-
MissingFlowError: If a flow name is provided and that flow does not exist
|
1673
|
-
UnspecifiedFlowError: If multiple flows exist but no flow name was provided
|
1674
|
-
"""
|
1675
|
-
return select_flow(
|
1676
|
-
load_flows_from_script(path),
|
1677
|
-
flow_name=flow_name,
|
1678
|
-
from_message=f"in script '{path}'",
|
1679
|
-
)
|
1680
|
-
|
1681
|
-
|
1682
1640
|
def load_flow_from_entrypoint(
|
1683
1641
|
entrypoint: str,
|
1684
1642
|
) -> Flow:
|
@@ -1696,52 +1654,25 @@ def load_flow_from_entrypoint(
|
|
1696
1654
|
FlowScriptError: If an exception is encountered while running the script
|
1697
1655
|
MissingFlowError: If the flow function specified in the entrypoint does not exist
|
1698
1656
|
"""
|
1699
|
-
with PrefectObjectRegistry( # type: ignore
|
1700
|
-
block_code_execution=True,
|
1701
|
-
capture_failures=True,
|
1702
|
-
):
|
1703
|
-
if ":" in entrypoint:
|
1704
|
-
# split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
|
1705
|
-
path, func_name = entrypoint.rsplit(":", maxsplit=1)
|
1706
|
-
else:
|
1707
|
-
path, func_name = entrypoint.rsplit(".", maxsplit=1)
|
1708
|
-
try:
|
1709
|
-
flow = import_object(entrypoint)
|
1710
|
-
except AttributeError as exc:
|
1711
|
-
raise MissingFlowError(
|
1712
|
-
f"Flow function with name {func_name!r} not found in {path!r}. "
|
1713
|
-
) from exc
|
1714
|
-
|
1715
|
-
if not isinstance(flow, Flow):
|
1716
|
-
raise MissingFlowError(
|
1717
|
-
f"Function with name {func_name!r} is not a flow. Make sure that it is "
|
1718
|
-
"decorated with '@flow'."
|
1719
|
-
)
|
1720
|
-
|
1721
|
-
return flow
|
1722
1657
|
|
1658
|
+
if ":" in entrypoint:
|
1659
|
+
# split by the last colon once to handle Windows paths with drive letters i.e C:\path\to\file.py:do_stuff
|
1660
|
+
path, func_name = entrypoint.rsplit(":", maxsplit=1)
|
1661
|
+
else:
|
1662
|
+
path, func_name = entrypoint.rsplit(".", maxsplit=1)
|
1663
|
+
try:
|
1664
|
+
flow = import_object(entrypoint)
|
1665
|
+
except AttributeError as exc:
|
1666
|
+
raise MissingFlowError(
|
1667
|
+
f"Flow function with name {func_name!r} not found in {path!r}. "
|
1668
|
+
) from exc
|
1723
1669
|
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1670
|
+
if not isinstance(flow, Flow):
|
1671
|
+
raise MissingFlowError(
|
1672
|
+
f"Function with name {func_name!r} is not a flow. Make sure that it is "
|
1673
|
+
"decorated with '@flow'."
|
1674
|
+
)
|
1727
1675
|
|
1728
|
-
The script will be written to a temporary local file path so errors can refer
|
1729
|
-
to line numbers and contextual tracebacks can be provided.
|
1730
|
-
"""
|
1731
|
-
with NamedTemporaryFile(
|
1732
|
-
mode="wt" if isinstance(script_contents, str) else "wb",
|
1733
|
-
prefix=f"flow-script-{flow_name}",
|
1734
|
-
suffix=".py",
|
1735
|
-
delete=False,
|
1736
|
-
) as tmpfile:
|
1737
|
-
tmpfile.write(script_contents)
|
1738
|
-
tmpfile.flush()
|
1739
|
-
try:
|
1740
|
-
flow = load_flow_from_script(tmpfile.name, flow_name=flow_name)
|
1741
|
-
finally:
|
1742
|
-
# windows compat
|
1743
|
-
tmpfile.close()
|
1744
|
-
os.remove(tmpfile.name)
|
1745
1676
|
return flow
|
1746
1677
|
|
1747
1678
|
|
prefect/futures.py
CHANGED
@@ -10,7 +10,7 @@ from typing_extensions import TypeVar
|
|
10
10
|
from prefect.client.orchestration import get_client
|
11
11
|
from prefect.client.schemas.objects import TaskRun
|
12
12
|
from prefect.exceptions import ObjectNotFound
|
13
|
-
from prefect.logging.loggers import get_logger
|
13
|
+
from prefect.logging.loggers import get_logger, get_run_logger
|
14
14
|
from prefect.states import Pending, State
|
15
15
|
from prefect.task_runs import TaskRunWaiter
|
16
16
|
from prefect.utilities.annotations import quote
|
@@ -143,6 +143,18 @@ class PrefectConcurrentFuture(PrefectWrappedFuture[concurrent.futures.Future]):
|
|
143
143
|
_result = run_coro_as_sync(_result)
|
144
144
|
return _result
|
145
145
|
|
146
|
+
def __del__(self):
|
147
|
+
if self._final_state or self._wrapped_future.done():
|
148
|
+
return
|
149
|
+
try:
|
150
|
+
local_logger = get_run_logger()
|
151
|
+
except Exception:
|
152
|
+
local_logger = logger
|
153
|
+
local_logger.warning(
|
154
|
+
"A future was garbage collected before it resolved."
|
155
|
+
" Please call `.wait()` or `.result()` on futures to ensure they resolve.",
|
156
|
+
)
|
157
|
+
|
146
158
|
|
147
159
|
class PrefectDistributedFuture(PrefectFuture):
|
148
160
|
"""
|
@@ -404,7 +404,7 @@ class CloudRunPushProvisioner:
|
|
404
404
|
dedent(
|
405
405
|
f"""\
|
406
406
|
from prefect import flow
|
407
|
-
from prefect.
|
407
|
+
from prefect.docker import DockerImage
|
408
408
|
|
409
409
|
|
410
410
|
@flow(log_prints=True)
|
@@ -416,7 +416,7 @@ class CloudRunPushProvisioner:
|
|
416
416
|
my_flow.deploy(
|
417
417
|
name="my-deployment",
|
418
418
|
work_pool_name="{work_pool_name}",
|
419
|
-
image=
|
419
|
+
image=DockerImage(
|
420
420
|
name="my-image:latest",
|
421
421
|
platform="linux/amd64",
|
422
422
|
)
|
@@ -1042,7 +1042,7 @@ class ContainerInstancePushProvisioner:
|
|
1042
1042
|
dedent(
|
1043
1043
|
f"""\
|
1044
1044
|
from prefect import flow
|
1045
|
-
from prefect.
|
1045
|
+
from prefect.docker import DockerImage
|
1046
1046
|
|
1047
1047
|
|
1048
1048
|
@flow(log_prints=True)
|
@@ -1054,7 +1054,7 @@ class ContainerInstancePushProvisioner:
|
|
1054
1054
|
my_flow.deploy(
|
1055
1055
|
name="my-deployment",
|
1056
1056
|
work_pool_name="{work_pool_name}",
|
1057
|
-
image=
|
1057
|
+
image=DockerImage(
|
1058
1058
|
name="my-image:latest",
|
1059
1059
|
platform="linux/amd64",
|
1060
1060
|
)
|
@@ -950,7 +950,7 @@ class ContainerRepositoryResource:
|
|
950
950
|
dedent(
|
951
951
|
f"""\
|
952
952
|
from prefect import flow
|
953
|
-
from prefect.
|
953
|
+
from prefect.docker import DockerImage
|
954
954
|
|
955
955
|
|
956
956
|
@flow(log_prints=True)
|
@@ -962,7 +962,7 @@ class ContainerRepositoryResource:
|
|
962
962
|
my_flow.deploy(
|
963
963
|
name="my-deployment",
|
964
964
|
work_pool_name="{self._work_pool_name}",
|
965
|
-
image=
|
965
|
+
image=DockerImage(
|
966
966
|
name="{self._repository_name}:latest",
|
967
967
|
platform="linux/amd64",
|
968
968
|
)
|
prefect/records/result_store.py
CHANGED
@@ -44,6 +44,10 @@ class ResultFactoryStore(RecordStore):
|
|
44
44
|
raise ValueError("Result could not be read")
|
45
45
|
|
46
46
|
def write(self, key: str, value: Any) -> BaseResult:
|
47
|
-
if isinstance(value,
|
47
|
+
if isinstance(value, PersistedResult):
|
48
|
+
# if the value is already a persisted result, write it
|
49
|
+
value.write(_sync=True)
|
50
|
+
return value
|
51
|
+
elif isinstance(value, BaseResult):
|
48
52
|
return value
|
49
53
|
return run_coro_as_sync(self.result_factory.create_result(obj=value, key=key))
|
prefect/results.py
CHANGED
@@ -431,7 +431,11 @@ class ResultFactory(BaseModel):
|
|
431
431
|
|
432
432
|
@sync_compatible
|
433
433
|
async def create_result(
|
434
|
-
self,
|
434
|
+
self,
|
435
|
+
obj: R,
|
436
|
+
key: Optional[str] = None,
|
437
|
+
expiration: Optional[DateTime] = None,
|
438
|
+
defer_persistence: bool = False,
|
435
439
|
) -> Union[R, "BaseResult[R]"]:
|
436
440
|
"""
|
437
441
|
Create a result type for the given object.
|
@@ -464,6 +468,7 @@ class ResultFactory(BaseModel):
|
|
464
468
|
serializer=self.serializer,
|
465
469
|
cache_object=should_cache_object,
|
466
470
|
expiration=expiration,
|
471
|
+
defer_persistence=defer_persistence,
|
467
472
|
)
|
468
473
|
|
469
474
|
@sync_compatible
|
@@ -589,6 +594,19 @@ class PersistedResult(BaseResult):
|
|
589
594
|
expiration: Optional[DateTime] = None
|
590
595
|
|
591
596
|
_should_cache_object: bool = PrivateAttr(default=True)
|
597
|
+
_persisted: bool = PrivateAttr(default=False)
|
598
|
+
_storage_block: WritableFileSystem = PrivateAttr(default=None)
|
599
|
+
_serializer: Serializer = PrivateAttr(default=None)
|
600
|
+
|
601
|
+
def _cache_object(
|
602
|
+
self,
|
603
|
+
obj: Any,
|
604
|
+
storage_block: WritableFileSystem = None,
|
605
|
+
serializer: Serializer = None,
|
606
|
+
) -> None:
|
607
|
+
self._cache = obj
|
608
|
+
self._storage_block = storage_block
|
609
|
+
self._serializer = serializer
|
592
610
|
|
593
611
|
@sync_compatible
|
594
612
|
@inject_client
|
@@ -601,7 +619,7 @@ class PersistedResult(BaseResult):
|
|
601
619
|
return self._cache
|
602
620
|
|
603
621
|
blob = await self._read_blob(client=client)
|
604
|
-
obj = blob.
|
622
|
+
obj = blob.load()
|
605
623
|
self.expiration = blob.expiration
|
606
624
|
|
607
625
|
if self._should_cache_object:
|
@@ -632,6 +650,46 @@ class PersistedResult(BaseResult):
|
|
632
650
|
if hasattr(storage_block, "_remote_file_system"):
|
633
651
|
return storage_block._remote_file_system._resolve_path(key)
|
634
652
|
|
653
|
+
@sync_compatible
|
654
|
+
@inject_client
|
655
|
+
async def write(self, obj: R = NotSet, client: "PrefectClient" = None) -> None:
|
656
|
+
"""
|
657
|
+
Write the result to the storage block.
|
658
|
+
"""
|
659
|
+
|
660
|
+
if self._persisted:
|
661
|
+
# don't double write or overwrite
|
662
|
+
return
|
663
|
+
|
664
|
+
# load objects from a cache
|
665
|
+
|
666
|
+
# first the object itself
|
667
|
+
if obj is NotSet and not self.has_cached_object():
|
668
|
+
raise ValueError("Cannot write a result that has no object cached.")
|
669
|
+
obj = obj if obj is not NotSet else self._cache
|
670
|
+
|
671
|
+
# next, the storage block
|
672
|
+
storage_block = self._storage_block
|
673
|
+
if storage_block is None:
|
674
|
+
block_document = await client.read_block_document(self.storage_block_id)
|
675
|
+
storage_block = Block._from_block_document(block_document)
|
676
|
+
|
677
|
+
# finally, the serializer
|
678
|
+
serializer = self._serializer
|
679
|
+
if serializer is None:
|
680
|
+
# this could error if the serializer requires kwargs
|
681
|
+
serializer = Serializer(type=self.serializer_type)
|
682
|
+
|
683
|
+
data = serializer.dumps(obj)
|
684
|
+
blob = PersistedResultBlob(
|
685
|
+
serializer=serializer, data=data, expiration=self.expiration
|
686
|
+
)
|
687
|
+
await storage_block.write_path(self.storage_key, content=blob.to_bytes())
|
688
|
+
self._persisted = True
|
689
|
+
|
690
|
+
if not self._should_cache_object:
|
691
|
+
self._cache = NotSet
|
692
|
+
|
635
693
|
@classmethod
|
636
694
|
@sync_compatible
|
637
695
|
async def create(
|
@@ -643,6 +701,7 @@ class PersistedResult(BaseResult):
|
|
643
701
|
serializer: Serializer,
|
644
702
|
cache_object: bool = True,
|
645
703
|
expiration: Optional[DateTime] = None,
|
704
|
+
defer_persistence: bool = False,
|
646
705
|
) -> "PersistedResult[R]":
|
647
706
|
"""
|
648
707
|
Create a new result reference from a user's object.
|
@@ -652,19 +711,13 @@ class PersistedResult(BaseResult):
|
|
652
711
|
"""
|
653
712
|
assert (
|
654
713
|
storage_block_id is not None
|
655
|
-
), "Unexpected storage block ID. Was it
|
656
|
-
data = serializer.dumps(obj)
|
657
|
-
blob = PersistedResultBlob(
|
658
|
-
serializer=serializer, data=data, expiration=expiration
|
659
|
-
)
|
714
|
+
), "Unexpected storage block ID. Was it saved?"
|
660
715
|
|
661
716
|
key = storage_key_fn()
|
662
717
|
if not isinstance(key, str):
|
663
718
|
raise TypeError(
|
664
719
|
f"Expected type 'str' for result storage key; got value {key!r}"
|
665
720
|
)
|
666
|
-
await storage_block.write_path(key, content=blob.to_bytes())
|
667
|
-
|
668
721
|
description = f"Result of type `{type(obj).__name__}`"
|
669
722
|
uri = cls._infer_path(storage_block, key)
|
670
723
|
if uri:
|
@@ -684,12 +737,23 @@ class PersistedResult(BaseResult):
|
|
684
737
|
expiration=expiration,
|
685
738
|
)
|
686
739
|
|
687
|
-
if cache_object:
|
740
|
+
if cache_object and not defer_persistence:
|
688
741
|
# Attach the object to the result so it's available without deserialization
|
689
|
-
result._cache_object(
|
742
|
+
result._cache_object(
|
743
|
+
obj, storage_block=storage_block, serializer=serializer
|
744
|
+
)
|
690
745
|
|
691
746
|
object.__setattr__(result, "_should_cache_object", cache_object)
|
692
747
|
|
748
|
+
if not defer_persistence:
|
749
|
+
await result.write(obj=obj)
|
750
|
+
else:
|
751
|
+
# we must cache temporarily to allow for writing later
|
752
|
+
# the cache will be removed on write
|
753
|
+
result._cache_object(
|
754
|
+
obj, storage_block=storage_block, serializer=serializer
|
755
|
+
)
|
756
|
+
|
693
757
|
return result
|
694
758
|
|
695
759
|
|
@@ -705,6 +769,9 @@ class PersistedResultBlob(BaseModel):
|
|
705
769
|
prefect_version: str = Field(default=prefect.__version__)
|
706
770
|
expiration: Optional[DateTime] = None
|
707
771
|
|
772
|
+
def load(self) -> Any:
|
773
|
+
return self.serializer.loads(self.data)
|
774
|
+
|
708
775
|
def to_bytes(self) -> bytes:
|
709
776
|
return self.model_dump_json(serialize_as_any=True).encode()
|
710
777
|
|
prefect/runner/runner.py
CHANGED
@@ -45,7 +45,7 @@ import threading
|
|
45
45
|
from copy import deepcopy
|
46
46
|
from functools import partial
|
47
47
|
from pathlib import Path
|
48
|
-
from typing import Callable, Dict, Iterable, List, Optional, Set, Union
|
48
|
+
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Set, Union
|
49
49
|
from uuid import UUID, uuid4
|
50
50
|
|
51
51
|
import anyio
|
@@ -75,7 +75,6 @@ from prefect.deployments.runner import (
|
|
75
75
|
EntrypointType,
|
76
76
|
RunnerDeployment,
|
77
77
|
)
|
78
|
-
from prefect.deployments.schedules import FlexibleScheduleList
|
79
78
|
from prefect.events import DeploymentTriggerTypes, TriggerTypes
|
80
79
|
from prefect.exceptions import Abort, ObjectNotFound
|
81
80
|
from prefect.flows import Flow, load_flow_from_flow_run
|
@@ -98,6 +97,9 @@ from prefect.utilities.engine import propose_state
|
|
98
97
|
from prefect.utilities.processutils import _register_signal, run_process
|
99
98
|
from prefect.utilities.services import critical_service_loop
|
100
99
|
|
100
|
+
if TYPE_CHECKING:
|
101
|
+
from prefect.client.types.flexible_schedule_list import FlexibleScheduleList
|
102
|
+
|
101
103
|
__all__ = ["Runner"]
|
102
104
|
|
103
105
|
|
@@ -221,7 +223,7 @@ class Runner:
|
|
221
223
|
cron: Optional[Union[Iterable[str], str]] = None,
|
222
224
|
rrule: Optional[Union[Iterable[str], str]] = None,
|
223
225
|
paused: Optional[bool] = None,
|
224
|
-
schedules: Optional[FlexibleScheduleList] = None,
|
226
|
+
schedules: Optional["FlexibleScheduleList"] = None,
|
225
227
|
schedule: Optional[SCHEDULE_TYPES] = None,
|
226
228
|
is_schedule_active: Optional[bool] = None,
|
227
229
|
parameters: Optional[dict] = None,
|
prefect/runner/server.py
CHANGED
@@ -10,7 +10,7 @@ from typing_extensions import Literal
|
|
10
10
|
from prefect._internal.schemas.validators import validate_values_conform_to_schema
|
11
11
|
from prefect.client.orchestration import get_client
|
12
12
|
from prefect.exceptions import MissingFlowError, ScriptError
|
13
|
-
from prefect.flows import Flow, load_flow_from_entrypoint
|
13
|
+
from prefect.flows import Flow, load_flow_from_entrypoint
|
14
14
|
from prefect.logging import get_logger
|
15
15
|
from prefect.runner.utils import (
|
16
16
|
inject_schemas_into_openapi,
|
@@ -24,6 +24,7 @@ from prefect.settings import (
|
|
24
24
|
PREFECT_RUNNER_SERVER_PORT,
|
25
25
|
)
|
26
26
|
from prefect.utilities.asyncutils import sync_compatible
|
27
|
+
from prefect.utilities.importtools import load_script_as_module
|
27
28
|
|
28
29
|
if TYPE_CHECKING:
|
29
30
|
from prefect.client.schemas.responses import DeploymentResponse
|
@@ -155,7 +156,10 @@ async def get_subflow_schemas(runner: "Runner") -> Dict[str, Dict]:
|
|
155
156
|
continue
|
156
157
|
|
157
158
|
script = deployment.entrypoint.split(":")[0]
|
158
|
-
|
159
|
+
module = load_script_as_module(script)
|
160
|
+
subflows = [
|
161
|
+
obj for obj in module.__dict__.values() if isinstance(obj, Flow)
|
162
|
+
]
|
159
163
|
for flow in subflows:
|
160
164
|
schemas[flow.name] = flow.parameters.model_dump()
|
161
165
|
|
prefect/states.py
CHANGED
@@ -209,6 +209,7 @@ async def return_value_to_state(
|
|
209
209
|
result_factory: ResultFactory,
|
210
210
|
key: Optional[str] = None,
|
211
211
|
expiration: Optional[datetime.datetime] = None,
|
212
|
+
defer_persistence: bool = False,
|
212
213
|
) -> State[R]:
|
213
214
|
"""
|
214
215
|
Given a return value from a user's function, create a `State` the run should
|
@@ -242,7 +243,10 @@ async def return_value_to_state(
|
|
242
243
|
# to update the data to the correct type
|
243
244
|
if not isinstance(state.data, BaseResult):
|
244
245
|
state.data = await result_factory.create_result(
|
245
|
-
state.data,
|
246
|
+
state.data,
|
247
|
+
key=key,
|
248
|
+
expiration=expiration,
|
249
|
+
defer_persistence=defer_persistence,
|
246
250
|
)
|
247
251
|
|
248
252
|
return state
|
@@ -284,7 +288,10 @@ async def return_value_to_state(
|
|
284
288
|
type=new_state_type,
|
285
289
|
message=message,
|
286
290
|
data=await result_factory.create_result(
|
287
|
-
retval,
|
291
|
+
retval,
|
292
|
+
key=key,
|
293
|
+
expiration=expiration,
|
294
|
+
defer_persistence=defer_persistence,
|
288
295
|
),
|
289
296
|
)
|
290
297
|
|
@@ -300,7 +307,10 @@ async def return_value_to_state(
|
|
300
307
|
else:
|
301
308
|
return Completed(
|
302
309
|
data=await result_factory.create_result(
|
303
|
-
data,
|
310
|
+
data,
|
311
|
+
key=key,
|
312
|
+
expiration=expiration,
|
313
|
+
defer_persistence=defer_persistence,
|
304
314
|
)
|
305
315
|
)
|
306
316
|
|