prefect-client 2.20.2__py3-none-any.whl → 3.0.0__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 +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +423 -164
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +667 -440
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2466
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +124 -51
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +138 -48
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.2.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
prefect/results.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
import abc
|
2
|
+
import inspect
|
2
3
|
import uuid
|
3
4
|
from functools import partial
|
4
5
|
from typing import (
|
5
6
|
TYPE_CHECKING,
|
6
7
|
Any,
|
7
|
-
Awaitable,
|
8
8
|
Callable,
|
9
9
|
Dict,
|
10
10
|
Generic,
|
@@ -16,20 +16,29 @@ from typing import (
|
|
16
16
|
)
|
17
17
|
from uuid import UUID
|
18
18
|
|
19
|
+
from pydantic import (
|
20
|
+
BaseModel,
|
21
|
+
ConfigDict,
|
22
|
+
Field,
|
23
|
+
PrivateAttr,
|
24
|
+
ValidationError,
|
25
|
+
model_serializer,
|
26
|
+
model_validator,
|
27
|
+
)
|
28
|
+
from pydantic_core import PydanticUndefinedType
|
29
|
+
from pydantic_extra_types.pendulum_dt import DateTime
|
19
30
|
from typing_extensions import ParamSpec, Self
|
20
31
|
|
21
32
|
import prefect
|
22
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
23
33
|
from prefect.blocks.core import Block
|
24
34
|
from prefect.client.utilities import inject_client
|
25
|
-
from prefect.exceptions import
|
35
|
+
from prefect.exceptions import MissingContextError, SerializationError
|
26
36
|
from prefect.filesystems import (
|
27
37
|
LocalFileSystem,
|
28
|
-
ReadableFileSystem,
|
29
38
|
WritableFileSystem,
|
30
39
|
)
|
31
40
|
from prefect.logging import get_logger
|
32
|
-
from prefect.serializers import Serializer
|
41
|
+
from prefect.serializers import PickleSerializer, Serializer
|
33
42
|
from prefect.settings import (
|
34
43
|
PREFECT_DEFAULT_RESULT_STORAGE_BLOCK,
|
35
44
|
PREFECT_LOCAL_STORAGE_PATH,
|
@@ -41,13 +50,6 @@ from prefect.utilities.annotations import NotSet
|
|
41
50
|
from prefect.utilities.asyncutils import sync_compatible
|
42
51
|
from prefect.utilities.pydantic import get_dispatch_key, lookup_type, register_base_type
|
43
52
|
|
44
|
-
if HAS_PYDANTIC_V2:
|
45
|
-
import pydantic.v1 as pydantic
|
46
|
-
|
47
|
-
else:
|
48
|
-
import pydantic
|
49
|
-
|
50
|
-
|
51
53
|
if TYPE_CHECKING:
|
52
54
|
from prefect import Flow, Task
|
53
55
|
from prefect.client.orchestration import PrefectClient
|
@@ -66,426 +68,555 @@ logger = get_logger("results")
|
|
66
68
|
P = ParamSpec("P")
|
67
69
|
R = TypeVar("R")
|
68
70
|
|
71
|
+
_default_storages: Dict[Tuple[str, str], WritableFileSystem] = {}
|
72
|
+
|
69
73
|
|
70
74
|
@sync_compatible
|
71
|
-
async def get_default_result_storage() ->
|
75
|
+
async def get_default_result_storage() -> WritableFileSystem:
|
72
76
|
"""
|
73
77
|
Generate a default file system for result storage.
|
74
78
|
"""
|
75
|
-
|
76
|
-
await Block.load(PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value())
|
77
|
-
if PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value() is not None
|
78
|
-
else LocalFileSystem(basepath=PREFECT_LOCAL_STORAGE_PATH.value())
|
79
|
-
)
|
79
|
+
default_block = PREFECT_DEFAULT_RESULT_STORAGE_BLOCK.value()
|
80
80
|
|
81
|
+
if default_block is not None:
|
82
|
+
return await resolve_result_storage(default_block)
|
81
83
|
|
82
|
-
|
84
|
+
# otherwise, use the local file system
|
85
|
+
basepath = PREFECT_LOCAL_STORAGE_PATH.value()
|
86
|
+
return LocalFileSystem(basepath=str(basepath))
|
83
87
|
|
84
88
|
|
85
|
-
|
89
|
+
@sync_compatible
|
90
|
+
async def resolve_result_storage(
|
91
|
+
result_storage: ResultStorage,
|
92
|
+
) -> WritableFileSystem:
|
86
93
|
"""
|
87
|
-
|
94
|
+
Resolve one of the valid `ResultStorage` input types into a saved block
|
95
|
+
document id and an instance of the block.
|
88
96
|
"""
|
89
|
-
|
90
|
-
PREFECT_TASK_SCHEDULING_DEFAULT_STORAGE_BLOCK.value(),
|
91
|
-
PREFECT_LOCAL_STORAGE_PATH.value(),
|
92
|
-
)
|
93
|
-
|
94
|
-
async def get_storage():
|
95
|
-
try:
|
96
|
-
return await Block.load(default_storage_name)
|
97
|
-
except ValueError as e:
|
98
|
-
if "Unable to find" not in str(e):
|
99
|
-
raise e
|
100
|
-
|
101
|
-
block_type_slug, name = default_storage_name.split("/")
|
102
|
-
if block_type_slug == "local-file-system":
|
103
|
-
block = LocalFileSystem(basepath=storage_path)
|
104
|
-
else:
|
105
|
-
raise Exception(
|
106
|
-
"The default task storage block does not exist, but it is of type "
|
107
|
-
f"'{block_type_slug}' which cannot be created implicitly. Please create "
|
108
|
-
"the block manually."
|
109
|
-
)
|
97
|
+
from prefect.client.orchestration import get_client
|
110
98
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
except ValueError as e:
|
115
|
-
if "already in use" not in str(e):
|
116
|
-
raise e
|
99
|
+
client = get_client()
|
100
|
+
if isinstance(result_storage, Block):
|
101
|
+
storage_block = result_storage
|
117
102
|
|
118
|
-
|
103
|
+
if storage_block._block_document_id is not None:
|
104
|
+
# Avoid saving the block if it already has an identifier assigned
|
105
|
+
storage_block_id = storage_block._block_document_id
|
106
|
+
else:
|
107
|
+
storage_block_id = None
|
108
|
+
elif isinstance(result_storage, str):
|
109
|
+
storage_block = await Block.load(result_storage, client=client)
|
110
|
+
storage_block_id = storage_block._block_document_id
|
111
|
+
assert storage_block_id is not None, "Loaded storage blocks must have ids"
|
112
|
+
else:
|
113
|
+
raise TypeError(
|
114
|
+
"Result storage must be one of the following types: 'UUID', 'Block', "
|
115
|
+
f"'str'. Got unsupported type {type(result_storage).__name__!r}."
|
116
|
+
)
|
119
117
|
|
120
|
-
|
121
|
-
return _default_task_scheduling_storages[cache_key]
|
122
|
-
except KeyError:
|
123
|
-
storage = await get_storage()
|
124
|
-
_default_task_scheduling_storages[cache_key] = storage
|
125
|
-
return storage
|
118
|
+
return storage_block
|
126
119
|
|
127
120
|
|
128
|
-
def
|
121
|
+
def resolve_serializer(serializer: ResultSerializer) -> Serializer:
|
129
122
|
"""
|
130
|
-
|
123
|
+
Resolve one of the valid `ResultSerializer` input types into a serializer
|
124
|
+
instance.
|
131
125
|
"""
|
132
|
-
|
126
|
+
if isinstance(serializer, Serializer):
|
127
|
+
return serializer
|
128
|
+
elif isinstance(serializer, str):
|
129
|
+
return Serializer(type=serializer)
|
130
|
+
else:
|
131
|
+
raise TypeError(
|
132
|
+
"Result serializer must be one of the following types: 'Serializer', "
|
133
|
+
f"'str'. Got unsupported type {type(serializer).__name__!r}."
|
134
|
+
)
|
133
135
|
|
134
136
|
|
135
|
-
def
|
137
|
+
async def get_or_create_default_task_scheduling_storage() -> ResultStorage:
|
136
138
|
"""
|
137
|
-
|
139
|
+
Generate a default file system for background task parameter/result storage.
|
138
140
|
"""
|
139
|
-
|
141
|
+
default_block = PREFECT_TASK_SCHEDULING_DEFAULT_STORAGE_BLOCK.value()
|
140
142
|
|
143
|
+
if default_block is not None:
|
144
|
+
return await Block.load(default_block)
|
141
145
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
persisted.
|
146
|
-
"""
|
147
|
-
if not flow.cache_result_in_memory:
|
148
|
-
return True
|
149
|
-
return False
|
146
|
+
# otherwise, use the local file system
|
147
|
+
basepath = PREFECT_LOCAL_STORAGE_PATH.value()
|
148
|
+
return LocalFileSystem(basepath=basepath)
|
150
149
|
|
151
150
|
|
152
|
-
def
|
151
|
+
def get_default_result_serializer() -> Serializer:
|
153
152
|
"""
|
154
|
-
|
155
|
-
runs to persist their results.
|
153
|
+
Generate a default file system for result storage.
|
156
154
|
"""
|
157
|
-
|
158
|
-
return True
|
159
|
-
return False
|
155
|
+
return resolve_serializer(PREFECT_RESULTS_DEFAULT_SERIALIZER.value())
|
160
156
|
|
161
157
|
|
162
|
-
def
|
158
|
+
def get_default_persist_setting() -> bool:
|
163
159
|
"""
|
164
|
-
|
165
|
-
persisted.
|
160
|
+
Return the default option for result persistence (False).
|
166
161
|
"""
|
167
|
-
|
168
|
-
return True
|
169
|
-
if not task.cache_result_in_memory:
|
170
|
-
return True
|
171
|
-
return False
|
162
|
+
return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
|
172
163
|
|
173
164
|
|
174
|
-
def _format_user_supplied_storage_key(key):
|
165
|
+
def _format_user_supplied_storage_key(key: str) -> str:
|
175
166
|
# Note here we are pinning to task runs since flow runs do not support storage keys
|
176
167
|
# yet; we'll need to split logic in the future or have two separate functions
|
177
168
|
runtime_vars = {key: getattr(prefect.runtime, key) for key in dir(prefect.runtime)}
|
178
169
|
return key.format(**runtime_vars, parameters=prefect.runtime.task_run.parameters)
|
179
170
|
|
180
171
|
|
181
|
-
class
|
172
|
+
class ResultStore(BaseModel):
|
182
173
|
"""
|
183
174
|
A utility to generate `Result` types.
|
184
175
|
"""
|
185
176
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
storage_key_fn: Callable[[], str]
|
177
|
+
result_storage: Optional[WritableFileSystem] = Field(default=None)
|
178
|
+
persist_result: bool = Field(default_factory=get_default_persist_setting)
|
179
|
+
cache_result_in_memory: bool = Field(default=True)
|
180
|
+
serializer: Serializer = Field(default_factory=get_default_result_serializer)
|
181
|
+
storage_key_fn: Callable[[], str] = Field(default=DEFAULT_STORAGE_KEY_FN)
|
192
182
|
|
193
|
-
@
|
194
|
-
|
195
|
-
|
183
|
+
@property
|
184
|
+
def result_storage_block_id(self) -> Optional[UUID]:
|
185
|
+
if self.result_storage is None:
|
186
|
+
return None
|
187
|
+
return self.result_storage._block_document_id
|
188
|
+
|
189
|
+
@sync_compatible
|
190
|
+
async def update_for_flow(self, flow: "Flow") -> Self:
|
196
191
|
"""
|
197
|
-
Create a new result
|
192
|
+
Create a new result store for a flow with updated settings.
|
198
193
|
|
199
|
-
|
200
|
-
|
194
|
+
Args:
|
195
|
+
flow: The flow to update the result store for.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
An updated result store.
|
201
199
|
"""
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
200
|
+
update = {}
|
201
|
+
if flow.result_storage is not None:
|
202
|
+
update["result_storage"] = await resolve_result_storage(flow.result_storage)
|
203
|
+
if flow.result_serializer is not None:
|
204
|
+
update["serializer"] = resolve_serializer(flow.result_serializer)
|
205
|
+
if flow.persist_result is not None:
|
206
|
+
update["persist_result"] = flow.persist_result
|
207
|
+
if flow.cache_result_in_memory is not None:
|
208
|
+
update["cache_result_in_memory"] = flow.cache_result_in_memory
|
209
|
+
if self.result_storage is None and update.get("result_storage") is None:
|
210
|
+
update["result_storage"] = await get_default_result_storage()
|
211
|
+
return self.model_copy(update=update)
|
206
212
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
kwargs.setdefault("cache_result_in_memory", True)
|
212
|
-
kwargs.setdefault("storage_key_fn", DEFAULT_STORAGE_KEY_FN)
|
213
|
+
@sync_compatible
|
214
|
+
async def update_for_task(self: Self, task: "Task") -> Self:
|
215
|
+
"""
|
216
|
+
Create a new result store for a task.
|
213
217
|
|
214
|
-
|
218
|
+
Args:
|
219
|
+
task: The task to update the result store for.
|
215
220
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
result_serializer=flow.result_serializer
|
232
|
-
or ctx.result_factory.serializer,
|
233
|
-
persist_result=(
|
234
|
-
flow.persist_result
|
235
|
-
if flow.persist_result is not None
|
236
|
-
# !! Child flows persist their result by default if the it or the
|
237
|
-
# parent flow uses a feature that requires it
|
238
|
-
else (
|
239
|
-
flow_features_require_result_persistence(flow)
|
240
|
-
or flow_features_require_child_result_persistence(ctx.flow)
|
241
|
-
or get_default_persist_setting()
|
242
|
-
)
|
243
|
-
),
|
244
|
-
cache_result_in_memory=flow.cache_result_in_memory,
|
245
|
-
storage_key_fn=DEFAULT_STORAGE_KEY_FN,
|
246
|
-
client=client,
|
247
|
-
)
|
248
|
-
else:
|
249
|
-
# This is a root flow run
|
250
|
-
# Pass the flow settings up to the default which will replace nulls with
|
251
|
-
# our default options
|
252
|
-
return await cls.default_factory(
|
253
|
-
client=client,
|
254
|
-
result_storage=flow.result_storage,
|
255
|
-
result_serializer=flow.result_serializer,
|
256
|
-
persist_result=(
|
257
|
-
flow.persist_result
|
258
|
-
if flow.persist_result is not None
|
259
|
-
# !! Flows persist their result by default if uses a feature that
|
260
|
-
# requires it
|
261
|
-
else (
|
262
|
-
flow_features_require_result_persistence(flow)
|
263
|
-
or get_default_persist_setting()
|
264
|
-
)
|
265
|
-
),
|
266
|
-
cache_result_in_memory=flow.cache_result_in_memory,
|
267
|
-
storage_key_fn=DEFAULT_STORAGE_KEY_FN,
|
221
|
+
Returns:
|
222
|
+
An updated result store.
|
223
|
+
"""
|
224
|
+
update = {}
|
225
|
+
if task.result_storage is not None:
|
226
|
+
update["result_storage"] = await resolve_result_storage(task.result_storage)
|
227
|
+
if task.result_serializer is not None:
|
228
|
+
update["serializer"] = resolve_serializer(task.result_serializer)
|
229
|
+
if task.persist_result is not None:
|
230
|
+
update["persist_result"] = task.persist_result
|
231
|
+
if task.cache_result_in_memory is not None:
|
232
|
+
update["cache_result_in_memory"] = task.cache_result_in_memory
|
233
|
+
if task.result_storage_key is not None:
|
234
|
+
update["storage_key_fn"] = partial(
|
235
|
+
_format_user_supplied_storage_key, task.result_storage_key
|
268
236
|
)
|
237
|
+
if self.result_storage is None and update.get("result_storage") is None:
|
238
|
+
update["result_storage"] = await get_default_result_storage()
|
239
|
+
return self.model_copy(update=update)
|
269
240
|
|
270
|
-
@
|
271
|
-
|
272
|
-
async def from_task(
|
273
|
-
cls: Type[Self], task: "Task", client: "PrefectClient" = None
|
274
|
-
) -> Self:
|
241
|
+
@sync_compatible
|
242
|
+
async def _read(self, key: str) -> "ResultRecord":
|
275
243
|
"""
|
276
|
-
|
244
|
+
Read a result record from storage.
|
245
|
+
|
246
|
+
This is the internal implementation. Use `read` or `aread` for synchronous and
|
247
|
+
asynchronous result reading respectively.
|
248
|
+
|
249
|
+
Args:
|
250
|
+
key: The key to read the result record from.
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
A result record.
|
277
254
|
"""
|
278
|
-
|
255
|
+
if self.result_storage is None:
|
256
|
+
self.result_storage = await get_default_result_storage()
|
279
257
|
|
280
|
-
|
258
|
+
content = await self.result_storage.read_path(f"{key}")
|
259
|
+
return ResultRecord.deserialize(content)
|
281
260
|
|
282
|
-
|
283
|
-
|
261
|
+
def read(self, key: str) -> "ResultRecord":
|
262
|
+
"""
|
263
|
+
Read a result record from storage.
|
284
264
|
|
285
|
-
|
265
|
+
Args:
|
266
|
+
key: The key to read the result record from.
|
286
267
|
|
287
|
-
|
288
|
-
|
289
|
-
async def from_autonomous_task(
|
290
|
-
cls: Type[Self], task: "Task[P, R]", client: "PrefectClient" = None
|
291
|
-
) -> Self:
|
268
|
+
Returns:
|
269
|
+
A result record.
|
292
270
|
"""
|
293
|
-
|
271
|
+
return self._read(key=key, _sync=True)
|
272
|
+
|
273
|
+
async def aread(self, key: str) -> "ResultRecord":
|
294
274
|
"""
|
295
|
-
|
296
|
-
task, get_or_create_default_task_scheduling_storage, client=client
|
297
|
-
)
|
275
|
+
Read a result record from storage.
|
298
276
|
|
299
|
-
|
300
|
-
|
301
|
-
async def _from_task(
|
302
|
-
cls: Type[Self],
|
303
|
-
task: "Task",
|
304
|
-
default_storage_getter: Callable[[], Awaitable[ResultStorage]],
|
305
|
-
client: "PrefectClient" = None,
|
306
|
-
) -> Self:
|
307
|
-
from prefect.context import FlowRunContext
|
308
|
-
|
309
|
-
ctx = FlowRunContext.get()
|
310
|
-
|
311
|
-
result_storage = task.result_storage or (
|
312
|
-
ctx.result_factory.storage_block
|
313
|
-
if ctx and ctx.result_factory
|
314
|
-
else await default_storage_getter()
|
315
|
-
)
|
316
|
-
result_serializer = task.result_serializer or (
|
317
|
-
ctx.result_factory.serializer
|
318
|
-
if ctx and ctx.result_factory
|
319
|
-
else get_default_result_serializer()
|
320
|
-
)
|
321
|
-
persist_result = (
|
322
|
-
task.persist_result
|
323
|
-
if task.persist_result is not None
|
324
|
-
# !! Tasks persist their result by default if their parent flow uses a
|
325
|
-
# feature that requires it or the task uses a feature that requires it
|
326
|
-
else (
|
327
|
-
(
|
328
|
-
flow_features_require_child_result_persistence(ctx.flow)
|
329
|
-
if ctx
|
330
|
-
else False
|
331
|
-
)
|
332
|
-
or task_features_require_result_persistence(task)
|
333
|
-
or get_default_persist_setting()
|
334
|
-
)
|
335
|
-
)
|
277
|
+
Args:
|
278
|
+
key: The key to read the result record from.
|
336
279
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
result_serializer=result_serializer,
|
342
|
-
persist_result=persist_result,
|
343
|
-
cache_result_in_memory=cache_result_in_memory,
|
344
|
-
client=client,
|
345
|
-
storage_key_fn=(
|
346
|
-
partial(_format_user_supplied_storage_key, task.result_storage_key)
|
347
|
-
if task.result_storage_key is not None
|
348
|
-
else DEFAULT_STORAGE_KEY_FN
|
349
|
-
),
|
350
|
-
)
|
280
|
+
Returns:
|
281
|
+
A result record.
|
282
|
+
"""
|
283
|
+
return await self._read(key=key, _sync=False)
|
351
284
|
|
352
|
-
@
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
client: "PrefectClient",
|
362
|
-
) -> Self:
|
363
|
-
storage_block_id, storage_block = await cls.resolve_storage_block(
|
364
|
-
result_storage, client=client, persist_result=persist_result
|
365
|
-
)
|
366
|
-
serializer = cls.resolve_serializer(result_serializer)
|
285
|
+
@sync_compatible
|
286
|
+
async def _write(
|
287
|
+
self,
|
288
|
+
obj: Any,
|
289
|
+
key: Optional[str] = None,
|
290
|
+
expiration: Optional[DateTime] = None,
|
291
|
+
):
|
292
|
+
"""
|
293
|
+
Write a result to storage.
|
367
294
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
295
|
+
This is the internal implementation. Use `write` or `awrite` for synchronous and
|
296
|
+
asynchronous result writing respectively.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
key: The key to write the result record to.
|
300
|
+
obj: The object to write to storage.
|
301
|
+
expiration: The expiration time for the result record.
|
302
|
+
"""
|
303
|
+
if self.result_storage is None:
|
304
|
+
self.result_storage = await get_default_result_storage()
|
305
|
+
key = key or self.storage_key_fn()
|
306
|
+
|
307
|
+
record = ResultRecord(
|
308
|
+
result=obj,
|
309
|
+
metadata=ResultRecordMetadata(
|
310
|
+
serializer=self.serializer, expiration=expiration, storage_key=key
|
311
|
+
),
|
375
312
|
)
|
313
|
+
await self.apersist_result_record(record)
|
376
314
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
client: "PrefectClient",
|
381
|
-
persist_result: bool = True,
|
382
|
-
) -> Tuple[Optional[uuid.UUID], WritableFileSystem]:
|
383
|
-
"""
|
384
|
-
Resolve one of the valid `ResultStorage` input types into a saved block
|
385
|
-
document id and an instance of the block.
|
386
|
-
"""
|
387
|
-
if isinstance(result_storage, Block):
|
388
|
-
storage_block = result_storage
|
389
|
-
|
390
|
-
if storage_block._block_document_id is not None:
|
391
|
-
# Avoid saving the block if it already has an identifier assigned
|
392
|
-
storage_block_id = storage_block._block_document_id
|
393
|
-
else:
|
394
|
-
if persist_result:
|
395
|
-
# TODO: Overwrite is true to avoid issues where the save collides with
|
396
|
-
# a previously saved document with a matching hash
|
397
|
-
storage_block_id = await storage_block._save(
|
398
|
-
is_anonymous=True, overwrite=True, client=client
|
399
|
-
)
|
400
|
-
else:
|
401
|
-
# a None-type UUID on unpersisted storage should not matter
|
402
|
-
# since the ID is generated on the server
|
403
|
-
storage_block_id = None
|
404
|
-
elif isinstance(result_storage, str):
|
405
|
-
storage_block = await Block.load(result_storage, client=client)
|
406
|
-
storage_block_id = storage_block._block_document_id
|
407
|
-
assert storage_block_id is not None, "Loaded storage blocks must have ids"
|
408
|
-
else:
|
409
|
-
raise TypeError(
|
410
|
-
"Result storage must be one of the following types: 'UUID', 'Block', "
|
411
|
-
f"'str'. Got unsupported type {type(result_storage).__name__!r}."
|
412
|
-
)
|
315
|
+
def write(self, key: str, obj: Any, expiration: Optional[DateTime] = None):
|
316
|
+
"""
|
317
|
+
Write a result to storage.
|
413
318
|
|
414
|
-
|
319
|
+
Handles the creation of a `ResultRecord` and its serialization to storage.
|
415
320
|
|
416
|
-
|
417
|
-
|
321
|
+
Args:
|
322
|
+
key: The key to write the result record to.
|
323
|
+
obj: The object to write to storage.
|
324
|
+
expiration: The expiration time for the result record.
|
418
325
|
"""
|
419
|
-
|
420
|
-
|
326
|
+
return self._write(obj=obj, key=key, expiration=expiration, _sync=True)
|
327
|
+
|
328
|
+
async def awrite(self, key: str, obj: Any, expiration: Optional[DateTime] = None):
|
421
329
|
"""
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
)
|
330
|
+
Write a result to storage.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
key: The key to write the result record to.
|
334
|
+
obj: The object to write to storage.
|
335
|
+
expiration: The expiration time for the result record.
|
336
|
+
"""
|
337
|
+
return await self._write(obj=obj, key=key, expiration=expiration, _sync=False)
|
431
338
|
|
432
339
|
@sync_compatible
|
433
|
-
async def
|
340
|
+
async def _persist_result_record(self, result_record: "ResultRecord"):
|
341
|
+
"""
|
342
|
+
Persist a result record to storage.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
result_record: The result record to persist.
|
346
|
+
"""
|
347
|
+
if self.result_storage is None:
|
348
|
+
self.result_storage = await get_default_result_storage()
|
349
|
+
|
350
|
+
await self.result_storage.write_path(
|
351
|
+
result_record.metadata.storage_key, content=result_record.serialize()
|
352
|
+
)
|
353
|
+
|
354
|
+
def persist_result_record(self, result_record: "ResultRecord"):
|
355
|
+
"""
|
356
|
+
Persist a result record to storage.
|
357
|
+
|
358
|
+
Args:
|
359
|
+
result_record: The result record to persist.
|
360
|
+
"""
|
361
|
+
return self._persist_result_record(result_record=result_record, _sync=True)
|
362
|
+
|
363
|
+
async def apersist_result_record(self, result_record: "ResultRecord"):
|
434
364
|
"""
|
435
|
-
|
365
|
+
Persist a result record to storage.
|
436
366
|
|
437
|
-
|
438
|
-
|
367
|
+
Args:
|
368
|
+
result_record: The result record to persist.
|
369
|
+
"""
|
370
|
+
return await self._persist_result_record(
|
371
|
+
result_record=result_record, _sync=False
|
372
|
+
)
|
439
373
|
|
440
|
-
|
441
|
-
|
442
|
-
|
374
|
+
@sync_compatible
|
375
|
+
async def create_result(
|
376
|
+
self,
|
377
|
+
obj: R,
|
378
|
+
key: Optional[str] = None,
|
379
|
+
expiration: Optional[DateTime] = None,
|
380
|
+
) -> Union[R, "BaseResult[R]"]:
|
381
|
+
"""
|
382
|
+
Create a `PersistedResult` for the given object.
|
443
383
|
"""
|
444
384
|
# Null objects are "cached" in memory at no cost
|
445
385
|
should_cache_object = self.cache_result_in_memory or obj is None
|
446
386
|
|
447
|
-
if
|
448
|
-
|
387
|
+
if key:
|
388
|
+
|
389
|
+
def key_fn():
|
390
|
+
return key
|
391
|
+
|
392
|
+
storage_key_fn = key_fn
|
393
|
+
else:
|
394
|
+
storage_key_fn = self.storage_key_fn
|
449
395
|
|
450
|
-
if
|
451
|
-
|
396
|
+
if self.result_storage is None:
|
397
|
+
self.result_storage = await get_default_result_storage()
|
452
398
|
|
453
399
|
return await PersistedResult.create(
|
454
400
|
obj,
|
455
|
-
storage_block=self.
|
456
|
-
storage_block_id=self.
|
457
|
-
storage_key_fn=
|
401
|
+
storage_block=self.result_storage,
|
402
|
+
storage_block_id=self.result_storage_block_id,
|
403
|
+
storage_key_fn=storage_key_fn,
|
458
404
|
serializer=self.serializer,
|
459
405
|
cache_object=should_cache_object,
|
406
|
+
expiration=expiration,
|
407
|
+
serialize_to_none=not self.persist_result,
|
460
408
|
)
|
461
409
|
|
410
|
+
# TODO: These two methods need to find a new home
|
411
|
+
|
462
412
|
@sync_compatible
|
463
413
|
async def store_parameters(self, identifier: UUID, parameters: Dict[str, Any]):
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
414
|
+
record = ResultRecord(
|
415
|
+
result=parameters,
|
416
|
+
metadata=ResultRecordMetadata(
|
417
|
+
serializer=self.serializer, storage_key=str(identifier)
|
418
|
+
),
|
419
|
+
)
|
420
|
+
await self.result_storage.write_path(
|
421
|
+
f"parameters/{identifier}", content=record.serialize()
|
471
422
|
)
|
472
423
|
|
473
424
|
@sync_compatible
|
474
425
|
async def read_parameters(self, identifier: UUID) -> Dict[str, Any]:
|
475
|
-
|
476
|
-
self.
|
477
|
-
)
|
478
|
-
|
479
|
-
|
426
|
+
record = ResultRecord.deserialize(
|
427
|
+
await self.result_storage.read_path(f"parameters/{identifier}")
|
428
|
+
)
|
429
|
+
return record.result
|
430
|
+
|
431
|
+
|
432
|
+
def get_current_result_store() -> ResultStore:
|
433
|
+
"""
|
434
|
+
Get the current result store.
|
435
|
+
"""
|
436
|
+
from prefect.context import get_run_context
|
437
|
+
|
438
|
+
try:
|
439
|
+
run_context = get_run_context()
|
440
|
+
except MissingContextError:
|
441
|
+
result_store = ResultStore()
|
442
|
+
else:
|
443
|
+
result_store = run_context.result_store
|
444
|
+
return result_store
|
445
|
+
|
446
|
+
|
447
|
+
class ResultRecordMetadata(BaseModel):
|
448
|
+
"""
|
449
|
+
Metadata for a result record.
|
450
|
+
"""
|
451
|
+
|
452
|
+
storage_key: Optional[str] = Field(
|
453
|
+
default=None
|
454
|
+
) # optional for backwards compatibility
|
455
|
+
expiration: Optional[DateTime] = Field(default=None)
|
456
|
+
serializer: Serializer = Field(default_factory=PickleSerializer)
|
457
|
+
prefect_version: str = Field(default=prefect.__version__)
|
458
|
+
|
459
|
+
def dump_bytes(self) -> bytes:
|
460
|
+
"""
|
461
|
+
Serialize the metadata to bytes.
|
462
|
+
|
463
|
+
Returns:
|
464
|
+
bytes: the serialized metadata
|
465
|
+
"""
|
466
|
+
return self.model_dump_json(serialize_as_any=True).encode()
|
467
|
+
|
468
|
+
@classmethod
|
469
|
+
def load_bytes(cls, data: bytes) -> "ResultRecordMetadata":
|
470
|
+
"""
|
471
|
+
Deserialize metadata from bytes.
|
472
|
+
|
473
|
+
Args:
|
474
|
+
data: the serialized metadata
|
475
|
+
|
476
|
+
Returns:
|
477
|
+
ResultRecordMetadata: the deserialized metadata
|
478
|
+
"""
|
479
|
+
return cls.model_validate_json(data)
|
480
|
+
|
481
|
+
|
482
|
+
class ResultRecord(BaseModel, Generic[R]):
|
483
|
+
"""
|
484
|
+
A record of a result.
|
485
|
+
"""
|
486
|
+
|
487
|
+
metadata: ResultRecordMetadata
|
488
|
+
result: R
|
489
|
+
|
490
|
+
@property
|
491
|
+
def expiration(self) -> Optional[DateTime]:
|
492
|
+
return self.metadata.expiration
|
493
|
+
|
494
|
+
@property
|
495
|
+
def serializer(self) -> Serializer:
|
496
|
+
return self.metadata.serializer
|
497
|
+
|
498
|
+
def serialize_result(self) -> bytes:
|
499
|
+
try:
|
500
|
+
data = self.serializer.dumps(self.result)
|
501
|
+
except Exception as exc:
|
502
|
+
extra_info = (
|
503
|
+
'You can try a different serializer (e.g. result_serializer="json") '
|
504
|
+
"or disabling persistence (persist_result=False) for this flow or task."
|
505
|
+
)
|
506
|
+
# check if this is a known issue with cloudpickle and pydantic
|
507
|
+
# and add extra information to help the user recover
|
508
|
+
|
509
|
+
if (
|
510
|
+
isinstance(exc, TypeError)
|
511
|
+
and isinstance(self.result, BaseModel)
|
512
|
+
and str(exc).startswith("cannot pickle")
|
513
|
+
):
|
514
|
+
try:
|
515
|
+
from IPython import get_ipython
|
516
|
+
|
517
|
+
if get_ipython() is not None:
|
518
|
+
extra_info = inspect.cleandoc(
|
519
|
+
"""
|
520
|
+
This is a known issue in Pydantic that prevents
|
521
|
+
locally-defined (non-imported) models from being
|
522
|
+
serialized by cloudpickle in IPython/Jupyter
|
523
|
+
environments. Please see
|
524
|
+
https://github.com/pydantic/pydantic/issues/8232 for
|
525
|
+
more information. To fix the issue, either: (1) move
|
526
|
+
your Pydantic class definition to an importable
|
527
|
+
location, (2) use the JSON serializer for your flow
|
528
|
+
or task (`result_serializer="json"`), or (3)
|
529
|
+
disable result persistence for your flow or task
|
530
|
+
(`persist_result=False`).
|
531
|
+
"""
|
532
|
+
).replace("\n", " ")
|
533
|
+
except ImportError:
|
534
|
+
pass
|
535
|
+
raise SerializationError(
|
536
|
+
f"Failed to serialize object of type {type(self.result).__name__!r} with "
|
537
|
+
f"serializer {self.serializer.type!r}. {extra_info}"
|
538
|
+
) from exc
|
539
|
+
|
540
|
+
return data
|
541
|
+
|
542
|
+
@model_validator(mode="before")
|
543
|
+
@classmethod
|
544
|
+
def coerce_old_format(cls, value: Any):
|
545
|
+
if isinstance(value, dict):
|
546
|
+
if "data" in value:
|
547
|
+
value["result"] = value.pop("data")
|
548
|
+
if "metadata" not in value:
|
549
|
+
value["metadata"] = {}
|
550
|
+
if "expiration" in value:
|
551
|
+
value["metadata"]["expiration"] = value.pop("expiration")
|
552
|
+
if "serializer" in value:
|
553
|
+
value["metadata"]["serializer"] = value.pop("serializer")
|
554
|
+
if "prefect_version" in value:
|
555
|
+
value["metadata"]["prefect_version"] = value.pop("prefect_version")
|
556
|
+
return value
|
557
|
+
|
558
|
+
def serialize_metadata(self) -> bytes:
|
559
|
+
return self.metadata.dump_bytes()
|
560
|
+
|
561
|
+
def serialize(
|
562
|
+
self,
|
563
|
+
) -> bytes:
|
564
|
+
"""
|
565
|
+
Serialize the record to bytes.
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
bytes: the serialized record
|
569
|
+
|
570
|
+
"""
|
571
|
+
return (
|
572
|
+
self.model_copy(update={"result": self.serialize_result()})
|
573
|
+
.model_dump_json(serialize_as_any=True)
|
574
|
+
.encode()
|
575
|
+
)
|
576
|
+
|
577
|
+
@classmethod
|
578
|
+
def deserialize(cls, data: bytes) -> "ResultRecord[R]":
|
579
|
+
"""
|
580
|
+
Deserialize a record from bytes.
|
581
|
+
|
582
|
+
Args:
|
583
|
+
data: the serialized record
|
584
|
+
|
585
|
+
Returns:
|
586
|
+
ResultRecord: the deserialized record
|
587
|
+
"""
|
588
|
+
instance = cls.model_validate_json(data)
|
589
|
+
if isinstance(instance.result, bytes):
|
590
|
+
instance.result = instance.serializer.loads(instance.result)
|
591
|
+
elif isinstance(instance.result, str):
|
592
|
+
instance.result = instance.serializer.loads(instance.result.encode())
|
593
|
+
return instance
|
594
|
+
|
595
|
+
@classmethod
|
596
|
+
def deserialize_from_result_and_metadata(
|
597
|
+
cls, result: bytes, metadata: bytes
|
598
|
+
) -> "ResultRecord[R]":
|
599
|
+
"""
|
600
|
+
Deserialize a record from separate result and metadata bytes.
|
601
|
+
|
602
|
+
Args:
|
603
|
+
result: the result
|
604
|
+
metadata: the serialized metadata
|
605
|
+
|
606
|
+
Returns:
|
607
|
+
ResultRecord: the deserialized record
|
608
|
+
"""
|
609
|
+
result_record_metadata = ResultRecordMetadata.load_bytes(metadata)
|
610
|
+
return cls(
|
611
|
+
metadata=result_record_metadata,
|
612
|
+
result=result_record_metadata.serializer.loads(result),
|
480
613
|
)
|
481
|
-
return self.serializer.loads(blob.data)
|
482
614
|
|
483
615
|
|
484
616
|
@register_base_type
|
485
|
-
class BaseResult(
|
617
|
+
class BaseResult(BaseModel, abc.ABC, Generic[R]):
|
618
|
+
model_config = ConfigDict(extra="forbid")
|
486
619
|
type: str
|
487
|
-
artifact_type: Optional[str]
|
488
|
-
artifact_description: Optional[str]
|
489
620
|
|
490
621
|
def __init__(self, **data: Any) -> None:
|
491
622
|
type_string = get_dispatch_key(self) if type(self) != BaseResult else "__base__"
|
@@ -497,12 +628,12 @@ class BaseResult(pydantic.BaseModel, abc.ABC, Generic[R]):
|
|
497
628
|
try:
|
498
629
|
subcls = lookup_type(cls, dispatch_key=kwargs["type"])
|
499
630
|
except KeyError as exc:
|
500
|
-
raise
|
631
|
+
raise ValidationError(errors=[exc], model=cls)
|
501
632
|
return super().__new__(subcls)
|
502
633
|
else:
|
503
634
|
return super().__new__(cls)
|
504
635
|
|
505
|
-
_cache: Any =
|
636
|
+
_cache: Any = PrivateAttr(NotSet)
|
506
637
|
|
507
638
|
def _cache_object(self, obj: Any) -> None:
|
508
639
|
self._cache = obj
|
@@ -524,79 +655,10 @@ class BaseResult(pydantic.BaseModel, abc.ABC, Generic[R]):
|
|
524
655
|
) -> "BaseResult[R]":
|
525
656
|
...
|
526
657
|
|
527
|
-
class Config:
|
528
|
-
extra = "forbid"
|
529
|
-
|
530
658
|
@classmethod
|
531
659
|
def __dispatch_key__(cls, **kwargs):
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
class UnpersistedResult(BaseResult):
|
536
|
-
"""
|
537
|
-
Result type for results that are not persisted outside of local memory.
|
538
|
-
"""
|
539
|
-
|
540
|
-
type = "unpersisted"
|
541
|
-
|
542
|
-
@sync_compatible
|
543
|
-
async def get(self) -> R:
|
544
|
-
if self.has_cached_object():
|
545
|
-
return self._cache
|
546
|
-
|
547
|
-
raise MissingResult("The result was not persisted and is no longer available.")
|
548
|
-
|
549
|
-
@classmethod
|
550
|
-
@sync_compatible
|
551
|
-
async def create(
|
552
|
-
cls: "Type[UnpersistedResult]",
|
553
|
-
obj: R,
|
554
|
-
cache_object: bool = True,
|
555
|
-
) -> "UnpersistedResult[R]":
|
556
|
-
description = f"Unpersisted result of type `{type(obj).__name__}`"
|
557
|
-
result = cls(
|
558
|
-
artifact_type="result",
|
559
|
-
artifact_description=description,
|
560
|
-
)
|
561
|
-
# Only store the object in local memory, it will not be sent to the API
|
562
|
-
if cache_object:
|
563
|
-
result._cache_object(obj)
|
564
|
-
return result
|
565
|
-
|
566
|
-
|
567
|
-
class LiteralResult(BaseResult):
|
568
|
-
"""
|
569
|
-
Result type for literal values like `None`, `True`, `False`.
|
570
|
-
|
571
|
-
These values are stored inline and JSON serialized when sent to the Prefect API.
|
572
|
-
They are not persisted to external result storage.
|
573
|
-
"""
|
574
|
-
|
575
|
-
type = "literal"
|
576
|
-
value: Any
|
577
|
-
|
578
|
-
def has_cached_object(self) -> bool:
|
579
|
-
# This result type always has the object cached in memory
|
580
|
-
return True
|
581
|
-
|
582
|
-
@sync_compatible
|
583
|
-
async def get(self) -> R:
|
584
|
-
return self.value
|
585
|
-
|
586
|
-
@classmethod
|
587
|
-
@sync_compatible
|
588
|
-
async def create(
|
589
|
-
cls: "Type[LiteralResult]",
|
590
|
-
obj: R,
|
591
|
-
) -> "LiteralResult[R]":
|
592
|
-
if type(obj) not in LITERAL_TYPES:
|
593
|
-
raise TypeError(
|
594
|
-
f"Unsupported type {type(obj).__name__!r} for result literal. Expected"
|
595
|
-
f" one of: {', '.join(type_.__name__ for type_ in LITERAL_TYPES)}"
|
596
|
-
)
|
597
|
-
|
598
|
-
description = f"Result with value `{obj}` persisted to Prefect."
|
599
|
-
return cls(value=obj, artifact_type="result", artifact_description=description)
|
660
|
+
default = cls.model_fields.get("type").get_default()
|
661
|
+
return cls.__name__ if isinstance(default, PydanticUndefinedType) else default
|
600
662
|
|
601
663
|
|
602
664
|
class PersistedResult(BaseResult):
|
@@ -604,47 +666,75 @@ class PersistedResult(BaseResult):
|
|
604
666
|
Result type which stores a reference to a persisted result.
|
605
667
|
|
606
668
|
When created, the user's object is serialized and stored. The format for the content
|
607
|
-
is defined by `
|
669
|
+
is defined by `ResultRecord`. This reference contains metadata necessary for retrieval
|
608
670
|
of the object, such as a reference to the storage block and the key where the
|
609
671
|
content was written.
|
610
672
|
"""
|
611
673
|
|
612
|
-
type = "reference"
|
674
|
+
type: str = "reference"
|
613
675
|
|
614
676
|
serializer_type: str
|
615
|
-
storage_block_id: uuid.UUID
|
616
677
|
storage_key: str
|
678
|
+
storage_block_id: Optional[uuid.UUID] = None
|
679
|
+
expiration: Optional[DateTime] = None
|
680
|
+
serialize_to_none: bool = False
|
681
|
+
|
682
|
+
_persisted: bool = PrivateAttr(default=False)
|
683
|
+
_should_cache_object: bool = PrivateAttr(default=True)
|
684
|
+
_storage_block: WritableFileSystem = PrivateAttr(default=None)
|
685
|
+
_serializer: Serializer = PrivateAttr(default=None)
|
686
|
+
|
687
|
+
@model_serializer(mode="wrap")
|
688
|
+
def serialize_model(self, handler, info):
|
689
|
+
if self.serialize_to_none:
|
690
|
+
return None
|
691
|
+
return handler(self, info)
|
692
|
+
|
693
|
+
def _cache_object(
|
694
|
+
self,
|
695
|
+
obj: Any,
|
696
|
+
storage_block: WritableFileSystem = None,
|
697
|
+
serializer: Serializer = None,
|
698
|
+
) -> None:
|
699
|
+
self._cache = obj
|
700
|
+
self._storage_block = storage_block
|
701
|
+
self._serializer = serializer
|
617
702
|
|
618
|
-
|
703
|
+
@inject_client
|
704
|
+
async def _get_storage_block(self, client: "PrefectClient") -> WritableFileSystem:
|
705
|
+
if self._storage_block is not None:
|
706
|
+
return self._storage_block
|
707
|
+
elif self.storage_block_id is not None:
|
708
|
+
block_document = await client.read_block_document(self.storage_block_id)
|
709
|
+
self._storage_block = Block._from_block_document(block_document)
|
710
|
+
else:
|
711
|
+
self._storage_block = await get_default_result_storage()
|
712
|
+
return self._storage_block
|
619
713
|
|
620
714
|
@sync_compatible
|
621
715
|
@inject_client
|
622
|
-
async def get(
|
716
|
+
async def get(
|
717
|
+
self, ignore_cache: bool = False, client: "PrefectClient" = None
|
718
|
+
) -> R:
|
623
719
|
"""
|
624
720
|
Retrieve the data and deserialize it into the original object.
|
625
721
|
"""
|
626
|
-
|
627
|
-
if self.has_cached_object():
|
722
|
+
if self.has_cached_object() and not ignore_cache:
|
628
723
|
return self._cache
|
629
724
|
|
630
|
-
|
631
|
-
|
725
|
+
result_store_kwargs = {}
|
726
|
+
if self._serializer:
|
727
|
+
result_store_kwargs["serializer"] = resolve_serializer(self._serializer)
|
728
|
+
storage_block = await self._get_storage_block(client=client)
|
729
|
+
result_store = ResultStore(result_storage=storage_block, **result_store_kwargs)
|
730
|
+
|
731
|
+
record = await result_store.aread(self.storage_key)
|
732
|
+
self.expiration = record.expiration
|
632
733
|
|
633
734
|
if self._should_cache_object:
|
634
|
-
self._cache_object(
|
735
|
+
self._cache_object(record.result)
|
635
736
|
|
636
|
-
return
|
637
|
-
|
638
|
-
@inject_client
|
639
|
-
async def _read_blob(self, client: "PrefectClient") -> "PersistedResultBlob":
|
640
|
-
assert (
|
641
|
-
self.storage_block_id is not None
|
642
|
-
), "Unexpected storage block ID. Was it persisted?"
|
643
|
-
block_document = await client.read_block_document(self.storage_block_id)
|
644
|
-
storage_block: ReadableFileSystem = Block._from_block_document(block_document)
|
645
|
-
content = await storage_block.read_path(self.storage_key)
|
646
|
-
blob = PersistedResultBlob.parse_raw(content)
|
647
|
-
return blob
|
737
|
+
return record.result
|
648
738
|
|
649
739
|
@staticmethod
|
650
740
|
def _infer_path(storage_block, key) -> str:
|
@@ -658,16 +748,55 @@ class PersistedResult(BaseResult):
|
|
658
748
|
if hasattr(storage_block, "_remote_file_system"):
|
659
749
|
return storage_block._remote_file_system._resolve_path(key)
|
660
750
|
|
751
|
+
@sync_compatible
|
752
|
+
@inject_client
|
753
|
+
async def write(self, obj: R = NotSet, client: "PrefectClient" = None) -> None:
|
754
|
+
"""
|
755
|
+
Write the result to the storage block.
|
756
|
+
"""
|
757
|
+
|
758
|
+
if self._persisted or self.serialize_to_none:
|
759
|
+
# don't double write or overwrite
|
760
|
+
return
|
761
|
+
|
762
|
+
# load objects from a cache
|
763
|
+
|
764
|
+
# first the object itself
|
765
|
+
if obj is NotSet and not self.has_cached_object():
|
766
|
+
raise ValueError("Cannot write a result that has no object cached.")
|
767
|
+
obj = obj if obj is not NotSet else self._cache
|
768
|
+
|
769
|
+
# next, the storage block
|
770
|
+
storage_block = await self._get_storage_block(client=client)
|
771
|
+
|
772
|
+
# finally, the serializer
|
773
|
+
serializer = self._serializer
|
774
|
+
if serializer is None:
|
775
|
+
# this could error if the serializer requires kwargs
|
776
|
+
serializer = Serializer(type=self.serializer_type)
|
777
|
+
|
778
|
+
result_store = ResultStore(result_storage=storage_block, serializer=serializer)
|
779
|
+
await result_store.awrite(
|
780
|
+
obj=obj, key=self.storage_key, expiration=self.expiration
|
781
|
+
)
|
782
|
+
|
783
|
+
self._persisted = True
|
784
|
+
|
785
|
+
if not self._should_cache_object:
|
786
|
+
self._cache = NotSet
|
787
|
+
|
661
788
|
@classmethod
|
662
789
|
@sync_compatible
|
663
790
|
async def create(
|
664
791
|
cls: "Type[PersistedResult]",
|
665
792
|
obj: R,
|
666
793
|
storage_block: WritableFileSystem,
|
667
|
-
storage_block_id: uuid.UUID,
|
668
794
|
storage_key_fn: Callable[[], str],
|
669
795
|
serializer: Serializer,
|
796
|
+
storage_block_id: Optional[uuid.UUID] = None,
|
670
797
|
cache_object: bool = True,
|
798
|
+
expiration: Optional[DateTime] = None,
|
799
|
+
serialize_to_none: bool = False,
|
671
800
|
) -> "PersistedResult[R]":
|
672
801
|
"""
|
673
802
|
Create a new result reference from a user's object.
|
@@ -675,95 +804,39 @@ class PersistedResult(BaseResult):
|
|
675
804
|
The object will be serialized and written to the storage block under a unique
|
676
805
|
key. It will then be cached on the returned result.
|
677
806
|
"""
|
678
|
-
assert (
|
679
|
-
storage_block_id is not None
|
680
|
-
), "Unexpected storage block ID. Was it persisted?"
|
681
|
-
data = serializer.dumps(obj)
|
682
|
-
blob = PersistedResultBlob(serializer=serializer, data=data)
|
683
|
-
|
684
807
|
key = storage_key_fn()
|
685
808
|
if not isinstance(key, str):
|
686
809
|
raise TypeError(
|
687
810
|
f"Expected type 'str' for result storage key; got value {key!r}"
|
688
811
|
)
|
689
|
-
|
690
|
-
await storage_block.write_path(key, content=blob.to_bytes())
|
691
|
-
|
692
|
-
description = f"Result of type `{type(obj).__name__}`"
|
693
812
|
uri = cls._infer_path(storage_block, key)
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
description += f" persisted to [{uri}]({uri})."
|
699
|
-
else:
|
700
|
-
description += f" persisted with storage block `{storage_block_id}`."
|
813
|
+
|
814
|
+
# in this case we store an absolute path
|
815
|
+
if storage_block_id is None and uri is not None:
|
816
|
+
key = str(uri)
|
701
817
|
|
702
818
|
result = cls(
|
703
819
|
serializer_type=serializer.type,
|
704
820
|
storage_block_id=storage_block_id,
|
705
821
|
storage_key=key,
|
706
|
-
|
707
|
-
|
822
|
+
expiration=expiration,
|
823
|
+
serialize_to_none=serialize_to_none,
|
708
824
|
)
|
709
825
|
|
710
|
-
if cache_object:
|
711
|
-
# Attach the object to the result so it's available without deserialization
|
712
|
-
result._cache_object(obj)
|
713
|
-
|
714
826
|
object.__setattr__(result, "_should_cache_object", cache_object)
|
827
|
+
# we must cache temporarily to allow for writing later
|
828
|
+
# the cache will be removed on write
|
829
|
+
result._cache_object(obj, storage_block=storage_block, serializer=serializer)
|
715
830
|
|
716
831
|
return result
|
717
832
|
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
prefect_version: str = pydantic.Field(default=prefect.__version__)
|
729
|
-
|
730
|
-
def to_bytes(self) -> bytes:
|
731
|
-
return self.json().encode()
|
732
|
-
|
733
|
-
|
734
|
-
class UnknownResult(BaseResult):
|
735
|
-
"""
|
736
|
-
Result type for unknown results. Typically used to represent the result
|
737
|
-
of tasks that were forced from a failure state into a completed state.
|
738
|
-
|
739
|
-
The value for this result is always None and is not persisted to external
|
740
|
-
result storage, but orchestration treats the result the same as persisted
|
741
|
-
results when determining orchestration rules, such as whether to rerun a
|
742
|
-
completed task.
|
743
|
-
"""
|
744
|
-
|
745
|
-
type = "unknown"
|
746
|
-
value: None
|
747
|
-
|
748
|
-
def has_cached_object(self) -> bool:
|
749
|
-
# This result type always has the object cached in memory
|
750
|
-
return True
|
751
|
-
|
752
|
-
@sync_compatible
|
753
|
-
async def get(self) -> R:
|
754
|
-
return self.value
|
755
|
-
|
756
|
-
@classmethod
|
757
|
-
@sync_compatible
|
758
|
-
async def create(
|
759
|
-
cls: "Type[UnknownResult]",
|
760
|
-
obj: R = None,
|
761
|
-
) -> "UnknownResult[R]":
|
762
|
-
if obj is not None:
|
763
|
-
raise TypeError(
|
764
|
-
f"Unsupported type {type(obj).__name__!r} for unknown result. "
|
765
|
-
"Only None is supported."
|
766
|
-
)
|
767
|
-
|
768
|
-
description = "Unknown result persisted to Prefect."
|
769
|
-
return cls(value=obj, artifact_type="result", artifact_description=description)
|
833
|
+
def __eq__(self, other):
|
834
|
+
if not isinstance(other, PersistedResult):
|
835
|
+
return False
|
836
|
+
return (
|
837
|
+
self.type == other.type
|
838
|
+
and self.serializer_type == other.serializer_type
|
839
|
+
and self.storage_key == other.storage_key
|
840
|
+
and self.storage_block_id == other.storage_block_id
|
841
|
+
and self.expiration == other.expiration
|
842
|
+
)
|