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.
Files changed (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +423 -164
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +667 -440
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2466
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +124 -51
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +138 -48
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.2.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {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 MissingResult
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() -> ResultStorage:
75
+ async def get_default_result_storage() -> WritableFileSystem:
72
76
  """
73
77
  Generate a default file system for result storage.
74
78
  """
75
- return (
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
- _default_task_scheduling_storages: Dict[Tuple[str, str], WritableFileSystem] = {}
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
- async def get_or_create_default_task_scheduling_storage() -> ResultStorage:
89
+ @sync_compatible
90
+ async def resolve_result_storage(
91
+ result_storage: ResultStorage,
92
+ ) -> WritableFileSystem:
86
93
  """
87
- Generate a default file system for autonomous task parameter/result storage.
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
- default_storage_name, storage_path = cache_key = (
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
- try:
112
- await block.save(name, overwrite=False)
113
- return block
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
- return await Block.load(default_storage_name)
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
- try:
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 get_default_result_serializer() -> ResultSerializer:
121
+ def resolve_serializer(serializer: ResultSerializer) -> Serializer:
129
122
  """
130
- Generate a default file system for result storage.
123
+ Resolve one of the valid `ResultSerializer` input types into a serializer
124
+ instance.
131
125
  """
132
- return PREFECT_RESULTS_DEFAULT_SERIALIZER.value()
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 get_default_persist_setting() -> bool:
137
+ async def get_or_create_default_task_scheduling_storage() -> ResultStorage:
136
138
  """
137
- Return the default option for result persistence (False).
139
+ Generate a default file system for background task parameter/result storage.
138
140
  """
139
- return PREFECT_RESULTS_PERSIST_BY_DEFAULT.value()
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
- def flow_features_require_result_persistence(flow: "Flow") -> bool:
143
- """
144
- Returns `True` if the given flow uses features that require its result to be
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 flow_features_require_child_result_persistence(flow: "Flow") -> bool:
151
+ def get_default_result_serializer() -> Serializer:
153
152
  """
154
- Returns `True` if the given flow uses features that require child flow and task
155
- runs to persist their results.
153
+ Generate a default file system for result storage.
156
154
  """
157
- if flow and flow.retries:
158
- return True
159
- return False
155
+ return resolve_serializer(PREFECT_RESULTS_DEFAULT_SERIALIZER.value())
160
156
 
161
157
 
162
- def task_features_require_result_persistence(task: "Task") -> bool:
158
+ def get_default_persist_setting() -> bool:
163
159
  """
164
- Returns `True` if the given task uses features that require its result to be
165
- persisted.
160
+ Return the default option for result persistence (False).
166
161
  """
167
- if task.cache_key_fn:
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 ResultFactory(pydantic.BaseModel):
172
+ class ResultStore(BaseModel):
182
173
  """
183
174
  A utility to generate `Result` types.
184
175
  """
185
176
 
186
- persist_result: bool
187
- cache_result_in_memory: bool
188
- serializer: Serializer
189
- storage_block_id: Optional[uuid.UUID]
190
- storage_block: WritableFileSystem
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
- @classmethod
194
- @inject_client
195
- async def default_factory(cls, client: "PrefectClient" = None, **kwargs):
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 factory with default options.
192
+ Create a new result store for a flow with updated settings.
198
193
 
199
- Keyword arguments may be provided to override defaults. Null keys will be
200
- ignored.
194
+ Args:
195
+ flow: The flow to update the result store for.
196
+
197
+ Returns:
198
+ An updated result store.
201
199
  """
202
- # Remove any null keys so `setdefault` can do its magic
203
- for key, value in tuple(kwargs.items()):
204
- if value is None:
205
- kwargs.pop(key)
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
- # Apply defaults
208
- kwargs.setdefault("result_storage", await get_default_result_storage())
209
- kwargs.setdefault("result_serializer", get_default_result_serializer())
210
- kwargs.setdefault("persist_result", get_default_persist_setting())
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
- return await cls.from_settings(**kwargs, client=client)
218
+ Args:
219
+ task: The task to update the result store for.
215
220
 
216
- @classmethod
217
- @inject_client
218
- async def from_flow(
219
- cls: Type[Self], flow: "Flow", client: "PrefectClient" = None
220
- ) -> Self:
221
- """
222
- Create a new result factory for a flow.
223
- """
224
- from prefect.context import FlowRunContext
225
-
226
- ctx = FlowRunContext.get()
227
- if ctx:
228
- # This is a child flow run
229
- return await cls.from_settings(
230
- result_storage=flow.result_storage or ctx.result_factory.storage_block,
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
- @classmethod
271
- @inject_client
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
- Create a new result factory for a task.
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
- from prefect.context import FlowRunContext
255
+ if self.result_storage is None:
256
+ self.result_storage = await get_default_result_storage()
279
257
 
280
- ctx = FlowRunContext.get()
258
+ content = await self.result_storage.read_path(f"{key}")
259
+ return ResultRecord.deserialize(content)
281
260
 
282
- if ctx and ctx.autonomous_task_run:
283
- return await cls.from_autonomous_task(task, client=client)
261
+ def read(self, key: str) -> "ResultRecord":
262
+ """
263
+ Read a result record from storage.
284
264
 
285
- return await cls._from_task(task, get_default_result_storage, client=client)
265
+ Args:
266
+ key: The key to read the result record from.
286
267
 
287
- @classmethod
288
- @inject_client
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
- Create a new result factory for an autonomous task.
271
+ return self._read(key=key, _sync=True)
272
+
273
+ async def aread(self, key: str) -> "ResultRecord":
294
274
  """
295
- return await cls._from_task(
296
- task, get_or_create_default_task_scheduling_storage, client=client
297
- )
275
+ Read a result record from storage.
298
276
 
299
- @classmethod
300
- @inject_client
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
- cache_result_in_memory = task.cache_result_in_memory
338
-
339
- return await cls.from_settings(
340
- result_storage=result_storage,
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
- @classmethod
353
- @inject_client
354
- async def from_settings(
355
- cls: Type[Self],
356
- result_storage: ResultStorage,
357
- result_serializer: ResultSerializer,
358
- persist_result: bool,
359
- cache_result_in_memory: bool,
360
- storage_key_fn: Callable[[], str],
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
- return cls(
369
- storage_block=storage_block,
370
- storage_block_id=storage_block_id,
371
- serializer=serializer,
372
- persist_result=persist_result,
373
- cache_result_in_memory=cache_result_in_memory,
374
- storage_key_fn=storage_key_fn,
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
- @staticmethod
378
- async def resolve_storage_block(
379
- result_storage: ResultStorage,
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
- return storage_block_id, storage_block
319
+ Handles the creation of a `ResultRecord` and its serialization to storage.
415
320
 
416
- @staticmethod
417
- def resolve_serializer(serializer: ResultSerializer) -> Serializer:
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
- Resolve one of the valid `ResultSerializer` input types into a serializer
420
- instance.
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
- if isinstance(serializer, Serializer):
423
- return serializer
424
- elif isinstance(serializer, str):
425
- return Serializer(type=serializer)
426
- else:
427
- raise TypeError(
428
- "Result serializer must be one of the following types: 'Serializer', "
429
- f"'str'. Got unsupported type {type(serializer).__name__!r}."
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 create_result(self, obj: R) -> Union[R, "BaseResult[R]"]:
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
- Create a result type for the given object.
365
+ Persist a result record to storage.
436
366
 
437
- If persistence is disabled, the object is wrapped in an `UnpersistedResult` and
438
- returned.
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
- If persistence is enabled:
441
- - Bool and null types are converted into `LiteralResult`.
442
- - Other types are serialized, persisted to storage, and a reference is returned.
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 not self.persist_result:
448
- return await UnpersistedResult.create(obj, cache_object=should_cache_object)
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 type(obj) in LITERAL_TYPES:
451
- return await LiteralResult.create(obj)
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.storage_block,
456
- storage_block_id=self.storage_block_id,
457
- storage_key_fn=self.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
- assert (
465
- self.storage_block_id is not None
466
- ), "Unexpected storage block ID. Was it persisted?"
467
- data = self.serializer.dumps(parameters)
468
- blob = PersistedResultBlob(serializer=self.serializer, data=data)
469
- await self.storage_block.write_path(
470
- f"parameters/{identifier}", content=blob.to_bytes()
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
- assert (
476
- self.storage_block_id is not None
477
- ), "Unexpected storage block ID. Was it persisted?"
478
- blob = PersistedResultBlob.parse_raw(
479
- await self.storage_block.read_path(f"parameters/{identifier}")
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(pydantic.BaseModel, abc.ABC, Generic[R]):
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 pydantic.ValidationError(errors=[exc], model=cls)
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 = pydantic.PrivateAttr(NotSet)
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
- return cls.__fields__.get("type").get_default()
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 `PersistedResultBlob`. This reference contains metadata necessary for retrieval
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
- _should_cache_object: bool = pydantic.PrivateAttr(default=True)
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(self, client: "PrefectClient") -> R:
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
- blob = await self._read_blob(client=client)
631
- obj = blob.serializer.loads(blob.data)
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(obj)
735
+ self._cache_object(record.result)
635
736
 
636
- return obj
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
- if uri:
695
- if isinstance(storage_block, LocalFileSystem):
696
- description += f" persisted to: `{uri}`"
697
- else:
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
- artifact_type="result",
707
- artifact_description=description,
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
- class PersistedResultBlob(pydantic.BaseModel):
720
- """
721
- The format of the content stored by a persisted result.
722
-
723
- Typically, this is written to a file as bytes.
724
- """
725
-
726
- serializer: Serializer
727
- data: bytes
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
+ )