prefect-client 3.0.0rc4__py3-none-any.whl → 3.0.0rc5__py3-none-any.whl

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