prefect-client 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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 (69) hide show
  1. prefect/__init__.py +0 -1
  2. prefect/_internal/compatibility/migration.py +124 -0
  3. prefect/_internal/concurrency/__init__.py +2 -2
  4. prefect/_internal/concurrency/primitives.py +1 -0
  5. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  6. prefect/_internal/pytz.py +1 -1
  7. prefect/blocks/core.py +1 -1
  8. prefect/client/orchestration.py +96 -22
  9. prefect/client/schemas/actions.py +1 -1
  10. prefect/client/schemas/filters.py +6 -0
  11. prefect/client/schemas/objects.py +10 -3
  12. prefect/client/subscriptions.py +6 -5
  13. prefect/context.py +1 -27
  14. prefect/deployments/__init__.py +3 -0
  15. prefect/deployments/base.py +4 -2
  16. prefect/deployments/deployments.py +3 -0
  17. prefect/deployments/steps/pull.py +1 -0
  18. prefect/deployments/steps/utility.py +2 -1
  19. prefect/engine.py +3 -0
  20. prefect/events/cli/automations.py +1 -1
  21. prefect/events/clients.py +7 -1
  22. prefect/exceptions.py +9 -0
  23. prefect/filesystems.py +22 -11
  24. prefect/flow_engine.py +195 -153
  25. prefect/flows.py +95 -36
  26. prefect/futures.py +9 -1
  27. prefect/infrastructure/provisioners/container_instance.py +1 -0
  28. prefect/infrastructure/provisioners/ecs.py +2 -2
  29. prefect/input/__init__.py +4 -0
  30. prefect/logging/formatters.py +2 -2
  31. prefect/logging/handlers.py +2 -2
  32. prefect/logging/loggers.py +1 -1
  33. prefect/plugins.py +1 -0
  34. prefect/records/cache_policies.py +3 -3
  35. prefect/records/result_store.py +10 -3
  36. prefect/results.py +47 -73
  37. prefect/runner/runner.py +1 -1
  38. prefect/runner/server.py +1 -1
  39. prefect/runtime/__init__.py +1 -0
  40. prefect/runtime/deployment.py +1 -0
  41. prefect/runtime/flow_run.py +1 -0
  42. prefect/runtime/task_run.py +1 -0
  43. prefect/settings.py +16 -3
  44. prefect/states.py +15 -4
  45. prefect/task_engine.py +195 -39
  46. prefect/task_runners.py +9 -3
  47. prefect/task_runs.py +26 -12
  48. prefect/task_worker.py +149 -20
  49. prefect/tasks.py +153 -71
  50. prefect/transactions.py +85 -15
  51. prefect/types/__init__.py +10 -3
  52. prefect/utilities/asyncutils.py +3 -3
  53. prefect/utilities/callables.py +16 -4
  54. prefect/utilities/collections.py +120 -57
  55. prefect/utilities/dockerutils.py +5 -3
  56. prefect/utilities/engine.py +11 -0
  57. prefect/utilities/filesystem.py +4 -5
  58. prefect/utilities/importtools.py +29 -0
  59. prefect/utilities/services.py +2 -2
  60. prefect/utilities/urls.py +195 -0
  61. prefect/utilities/visualization.py +1 -0
  62. prefect/variables.py +4 -0
  63. prefect/workers/base.py +35 -0
  64. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/METADATA +2 -2
  65. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/RECORD +68 -66
  66. prefect/blocks/kubernetes.py +0 -115
  67. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/LICENSE +0 -0
  68. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/WHEEL +0 -0
  69. {prefect_client-3.0.0rc2.dist-info → prefect_client-3.0.0rc4.dist-info}/top_level.txt +0 -0
prefect/flows.py CHANGED
@@ -4,7 +4,6 @@ Module containing the base workflow class and decorator - for most use cases, us
4
4
 
5
5
  # This file requires type-checking with pyright because mypy does not yet support PEP612
6
6
  # See https://github.com/python/mypy/issues/8645
7
-
8
7
  import ast
9
8
  import datetime
10
9
  import importlib.util
@@ -15,6 +14,7 @@ import re
15
14
  import sys
16
15
  import tempfile
17
16
  import warnings
17
+ from copy import copy
18
18
  from functools import partial, update_wrapper
19
19
  from pathlib import Path
20
20
  from tempfile import NamedTemporaryFile
@@ -89,7 +89,6 @@ from prefect.task_runners import TaskRunner, ThreadPoolTaskRunner
89
89
  from prefect.types import BANNED_CHARACTERS, WITHOUT_BANNED_CHARACTERS
90
90
  from prefect.utilities.annotations import NotSet
91
91
  from prefect.utilities.asyncutils import (
92
- is_async_fn,
93
92
  run_sync_in_worker_thread,
94
93
  sync_compatible,
95
94
  )
@@ -102,7 +101,7 @@ from prefect.utilities.callables import (
102
101
  from prefect.utilities.collections import listrepr
103
102
  from prefect.utilities.filesystem import relative_path_to_current_platform
104
103
  from prefect.utilities.hashing import file_hash
105
- from prefect.utilities.importtools import import_object
104
+ from prefect.utilities.importtools import import_object, safe_load_namespace
106
105
 
107
106
  from ._internal.pydantic.v2_schema import is_v2_type
108
107
  from ._internal.pydantic.v2_validated_func import V2ValidatedFunction
@@ -289,7 +288,18 @@ class Flow(Generic[P, R]):
289
288
  self.description = description or inspect.getdoc(fn)
290
289
  update_wrapper(self, fn)
291
290
  self.fn = fn
292
- self.isasync = is_async_fn(self.fn)
291
+
292
+ # the flow is considered async if its function is async or an async
293
+ # generator
294
+ self.isasync = inspect.iscoroutinefunction(
295
+ self.fn
296
+ ) or inspect.isasyncgenfunction(self.fn)
297
+
298
+ # the flow is considered a generator if its function is a generator or
299
+ # an async generator
300
+ self.isgenerator = inspect.isgeneratorfunction(
301
+ self.fn
302
+ ) or inspect.isasyncgenfunction(self.fn)
293
303
 
294
304
  raise_for_reserved_arguments(self.fn, ["return_state", "wait_for"])
295
305
 
@@ -354,6 +364,28 @@ class Flow(Generic[P, R]):
354
364
 
355
365
  self._entrypoint = f"{module}:{fn.__name__}"
356
366
 
367
+ @property
368
+ def ismethod(self) -> bool:
369
+ return hasattr(self.fn, "__prefect_self__")
370
+
371
+ def __get__(self, instance, owner):
372
+ """
373
+ Implement the descriptor protocol so that the flow can be used as an instance method.
374
+ When an instance method is loaded, this method is called with the "self" instance as
375
+ an argument. We return a copy of the flow with that instance bound to the flow's function.
376
+ """
377
+
378
+ # if no instance is provided, it's being accessed on the class
379
+ if instance is None:
380
+ return self
381
+
382
+ # if the flow is being accessed on an instance, bind the instance to the __prefect_self__ attribute
383
+ # of the flow's function. This will allow it to be automatically added to the flow's parameters
384
+ else:
385
+ bound_flow = copy(self)
386
+ bound_flow.fn.__prefect_self__ = instance
387
+ return bound_flow
388
+
357
389
  def with_options(
358
390
  self,
359
391
  *,
@@ -555,6 +587,9 @@ class Flow(Generic[P, R]):
555
587
  """
556
588
  serialized_parameters = {}
557
589
  for key, value in parameters.items():
590
+ # do not serialize the bound self object
591
+ if self.ismethod and value is self.fn.__prefect_self__:
592
+ continue
558
593
  try:
559
594
  serialized_parameters[key] = jsonable_encoder(value)
560
595
  except (TypeError, ValueError):
@@ -1241,19 +1276,14 @@ class Flow(Generic[P, R]):
1241
1276
  # we can add support for exploring subflows for tasks in the future.
1242
1277
  return track_viz_task(self.isasync, self.name, parameters)
1243
1278
 
1244
- from prefect.flow_engine import run_flow, run_flow_sync
1279
+ from prefect.flow_engine import run_flow
1245
1280
 
1246
- run_kwargs = dict(
1281
+ return run_flow(
1247
1282
  flow=self,
1248
1283
  parameters=parameters,
1249
1284
  wait_for=wait_for,
1250
1285
  return_type=return_type,
1251
1286
  )
1252
- if self.isasync:
1253
- # this returns an awaitable coroutine
1254
- return run_flow(**run_kwargs)
1255
- else:
1256
- return run_flow_sync(**run_kwargs)
1257
1287
 
1258
1288
  @sync_compatible
1259
1289
  async def visualize(self, *args, **kwargs):
@@ -1329,8 +1359,8 @@ def flow(
1329
1359
  retries: Optional[int] = None,
1330
1360
  retry_delay_seconds: Optional[Union[int, float]] = None,
1331
1361
  task_runner: Optional[TaskRunner] = None,
1332
- description: str = None,
1333
- timeout_seconds: Union[int, float] = None,
1362
+ description: Optional[str] = None,
1363
+ timeout_seconds: Union[int, float, None] = None,
1334
1364
  validate_parameters: bool = True,
1335
1365
  persist_result: Optional[bool] = None,
1336
1366
  result_storage: Optional[ResultStorage] = None,
@@ -1358,11 +1388,11 @@ def flow(
1358
1388
  name: Optional[str] = None,
1359
1389
  version: Optional[str] = None,
1360
1390
  flow_run_name: Optional[Union[Callable[[], str], str]] = None,
1361
- retries: int = None,
1362
- retry_delay_seconds: Union[int, float] = None,
1391
+ retries: Optional[int] = None,
1392
+ retry_delay_seconds: Union[int, float, None] = None,
1363
1393
  task_runner: Optional[TaskRunner] = None,
1364
- description: str = None,
1365
- timeout_seconds: Union[int, float] = None,
1394
+ description: Optional[str] = None,
1395
+ timeout_seconds: Union[int, float, None] = None,
1366
1396
  validate_parameters: bool = True,
1367
1397
  persist_result: Optional[bool] = None,
1368
1398
  result_storage: Optional[ResultStorage] = None,
@@ -1485,6 +1515,9 @@ def flow(
1485
1515
  >>> pass
1486
1516
  """
1487
1517
  if __fn:
1518
+ if isinstance(__fn, (classmethod, staticmethod)):
1519
+ method_decorator = type(__fn).__name__
1520
+ raise TypeError(f"@{method_decorator} should be applied on top of @flow")
1488
1521
  return cast(
1489
1522
  Flow[P, R],
1490
1523
  Flow(
@@ -1560,7 +1593,9 @@ flow.from_source = Flow.from_source
1560
1593
 
1561
1594
 
1562
1595
  def select_flow(
1563
- flows: Iterable[Flow], flow_name: str = None, from_message: str = None
1596
+ flows: Iterable[Flow],
1597
+ flow_name: Optional[str] = None,
1598
+ from_message: Optional[str] = None,
1564
1599
  ) -> Flow:
1565
1600
  """
1566
1601
  Select the only flow in an iterable or a flow specified by name.
@@ -1574,33 +1609,33 @@ def select_flow(
1574
1609
  UnspecifiedFlowError: If multiple flows exist but no flow name was provided
1575
1610
  """
1576
1611
  # Convert to flows by name
1577
- flows = {f.name: f for f in flows}
1612
+ flows_dict = {f.name: f for f in flows}
1578
1613
 
1579
1614
  # Add a leading space if given, otherwise use an empty string
1580
1615
  from_message = (" " + from_message) if from_message else ""
1581
- if not flows:
1616
+ if not Optional:
1582
1617
  raise MissingFlowError(f"No flows found{from_message}.")
1583
1618
 
1584
- elif flow_name and flow_name not in flows:
1619
+ elif flow_name and flow_name not in flows_dict:
1585
1620
  raise MissingFlowError(
1586
1621
  f"Flow {flow_name!r} not found{from_message}. "
1587
- f"Found the following flows: {listrepr(flows.keys())}. "
1622
+ f"Found the following flows: {listrepr(flows_dict.keys())}. "
1588
1623
  "Check to make sure that your flow function is decorated with `@flow`."
1589
1624
  )
1590
1625
 
1591
- elif not flow_name and len(flows) > 1:
1626
+ elif not flow_name and len(flows_dict) > 1:
1592
1627
  raise UnspecifiedFlowError(
1593
1628
  (
1594
- f"Found {len(flows)} flows{from_message}:"
1595
- f" {listrepr(sorted(flows.keys()))}. Specify a flow name to select a"
1629
+ f"Found {len(flows_dict)} flows{from_message}:"
1630
+ f" {listrepr(sorted(flows_dict.keys()))}. Specify a flow name to select a"
1596
1631
  " flow."
1597
1632
  ),
1598
1633
  )
1599
1634
 
1600
1635
  if flow_name:
1601
- return flows[flow_name]
1636
+ return flows_dict[flow_name]
1602
1637
  else:
1603
- return list(flows.values())[0]
1638
+ return list(flows_dict.values())[0]
1604
1639
 
1605
1640
 
1606
1641
  def load_flows_from_script(path: str) -> List[Flow]:
@@ -1617,7 +1652,7 @@ def load_flows_from_script(path: str) -> List[Flow]:
1617
1652
  return registry_from_script(path).get_instances(Flow)
1618
1653
 
1619
1654
 
1620
- def load_flow_from_script(path: str, flow_name: str = None) -> Flow:
1655
+ def load_flow_from_script(path: str, flow_name: Optional[str] = None) -> Flow:
1621
1656
  """
1622
1657
  Extract a flow object from a script by running all of the code in the file.
1623
1658
 
@@ -1661,7 +1696,7 @@ def load_flow_from_entrypoint(
1661
1696
  FlowScriptError: If an exception is encountered while running the script
1662
1697
  MissingFlowError: If the flow function specified in the entrypoint does not exist
1663
1698
  """
1664
- with PrefectObjectRegistry(
1699
+ with PrefectObjectRegistry( # type: ignore
1665
1700
  block_code_execution=True,
1666
1701
  capture_failures=True,
1667
1702
  ):
@@ -1686,7 +1721,7 @@ def load_flow_from_entrypoint(
1686
1721
  return flow
1687
1722
 
1688
1723
 
1689
- def load_flow_from_text(script_contents: AnyStr, flow_name: str):
1724
+ def load_flow_from_text(script_contents: AnyStr, flow_name: str) -> Flow:
1690
1725
  """
1691
1726
  Load a flow from a text script.
1692
1727
 
@@ -1717,7 +1752,7 @@ async def serve(
1717
1752
  print_starting_message: bool = True,
1718
1753
  limit: Optional[int] = None,
1719
1754
  **kwargs,
1720
- ):
1755
+ ) -> NoReturn:
1721
1756
  """
1722
1757
  Serve the provided list of deployments.
1723
1758
 
@@ -1807,7 +1842,7 @@ async def load_flow_from_flow_run(
1807
1842
  flow_run: "FlowRun",
1808
1843
  ignore_storage: bool = False,
1809
1844
  storage_base_path: Optional[str] = None,
1810
- ) -> "Flow":
1845
+ ) -> Flow:
1811
1846
  """
1812
1847
  Load a flow from the location/script provided in a deployment's storage document.
1813
1848
 
@@ -1861,7 +1896,9 @@ async def load_flow_from_flow_run(
1861
1896
  await storage_block.get_directory(from_path=from_path, local_path=".")
1862
1897
 
1863
1898
  if deployment.pull_steps:
1864
- run_logger.debug(f"Running {len(deployment.pull_steps)} deployment pull steps")
1899
+ run_logger.debug(
1900
+ f"Running {len(deployment.pull_steps)} deployment pull step(s)"
1901
+ )
1865
1902
  output = await run_steps(deployment.pull_steps)
1866
1903
  if output.get("directory"):
1867
1904
  run_logger.debug(f"Changing working directory to {output['directory']!r}")
@@ -1933,11 +1970,33 @@ def load_flow_argument_from_entrypoint(
1933
1970
  ):
1934
1971
  for keyword in decorator.keywords:
1935
1972
  if keyword.arg == arg:
1936
- return (
1937
- keyword.value.value
1938
- ) # Return the string value of the argument
1973
+ if isinstance(keyword.value, ast.Constant):
1974
+ return (
1975
+ keyword.value.value
1976
+ ) # Return the string value of the argument
1977
+
1978
+ # if the arg value is not a raw str (i.e. a variable or expression),
1979
+ # then attempt to evaluate it
1980
+ namespace = safe_load_namespace(source_code)
1981
+ literal_arg_value = ast.get_source_segment(
1982
+ source_code, keyword.value
1983
+ )
1984
+ try:
1985
+ evaluated_value = eval(literal_arg_value, namespace) # type: ignore
1986
+ except Exception as e:
1987
+ logger.info(
1988
+ "Failed to parse @flow argument: `%s=%s` due to the following error. Ignoring and falling back to default behavior.",
1989
+ arg,
1990
+ literal_arg_value,
1991
+ exc_info=e,
1992
+ )
1993
+ # ignore the decorator arg and fallback to default behavior
1994
+ break
1995
+ return str(evaluated_value)
1939
1996
 
1940
1997
  if arg == "name":
1941
1998
  return func_name.replace(
1942
1999
  "_", "-"
1943
2000
  ) # If no matching decorator or keyword argument is found
2001
+
2002
+ return None
prefect/futures.py CHANGED
@@ -56,7 +56,7 @@ class PrefectFuture(abc.ABC):
56
56
  def wait(self, timeout: Optional[float] = None) -> None:
57
57
  ...
58
58
  """
59
- Wait for the task run to complete.
59
+ Wait for the task run to complete.
60
60
 
61
61
  If the task run has already completed, this method will return immediately.
62
62
 
@@ -163,6 +163,10 @@ class PrefectDistributedFuture(PrefectFuture):
163
163
  )
164
164
  return
165
165
 
166
+ # Ask for the instance of TaskRunWaiter _now_ so that it's already running and
167
+ # can catch the completion event if it happens before we start listening for it.
168
+ TaskRunWaiter.instance()
169
+
166
170
  # Read task run to see if it is still running
167
171
  async with get_client() as client:
168
172
  task_run = await client.read_task_run(task_run_id=self._task_run_id)
@@ -245,6 +249,10 @@ def resolve_futures_to_states(
245
249
  context={},
246
250
  )
247
251
 
252
+ # if no futures were found, return the original expression
253
+ if not futures:
254
+ return expr
255
+
248
256
  # Get final states for each future
249
257
  states = []
250
258
  for future in futures:
@@ -10,6 +10,7 @@ Classes:
10
10
  ContainerInstancePushProvisioner: A class for provisioning infrastructure using Azure Container Instances.
11
11
 
12
12
  """
13
+
13
14
  import json
14
15
  import random
15
16
  import shlex
@@ -367,7 +367,7 @@ class AuthenticationResource:
367
367
  work_pool_name: str,
368
368
  user_name: str = "prefect-ecs-user",
369
369
  policy_name: str = "prefect-ecs-policy",
370
- credentials_block_name: str = None,
370
+ credentials_block_name: Optional[str] = None,
371
371
  ):
372
372
  self._user_name = user_name
373
373
  self._credentials_block_name = (
@@ -1130,7 +1130,7 @@ class ElasticContainerServicePushProvisioner:
1130
1130
  work_pool_name: str,
1131
1131
  user_name: str = "prefect-ecs-user",
1132
1132
  policy_name: str = "prefect-ecs-policy",
1133
- credentials_block_name: str = None,
1133
+ credentials_block_name: Optional[str] = None,
1134
1134
  cluster_name: str = "prefect-ecs-cluster",
1135
1135
  vpc_name: str = "prefect-ecs-vpc",
1136
1136
  ecs_security_group_name: str = "prefect-ecs-security-group",
prefect/input/__init__.py CHANGED
@@ -12,6 +12,8 @@ from .run_input import (
12
12
  RunInputMetadata,
13
13
  keyset_from_base_key,
14
14
  keyset_from_paused_state,
15
+ receive_input,
16
+ send_input,
15
17
  )
16
18
 
17
19
  __all__ = [
@@ -26,4 +28,6 @@ __all__ = [
26
28
  "keyset_from_base_key",
27
29
  "keyset_from_paused_state",
28
30
  "read_flow_run_input",
31
+ "receive_input",
32
+ "send_input",
29
33
  ]
@@ -78,8 +78,8 @@ class PrefectFormatter(logging.Formatter):
78
78
  validate=True,
79
79
  *,
80
80
  defaults=None,
81
- task_run_fmt: str = None,
82
- flow_run_fmt: str = None,
81
+ task_run_fmt: Optional[str] = None,
82
+ flow_run_fmt: Optional[str] = None,
83
83
  ) -> None:
84
84
  """
85
85
  Implementation of the standard Python formatter with support for multiple
@@ -108,8 +108,8 @@ class APILogHandler(logging.Handler):
108
108
  )
109
109
 
110
110
  # Not ideal, but this method is called by the stdlib and cannot return a
111
- # coroutine so we just schedule the drain in a new thread and continue
112
- from_sync.call_soon_in_new_thread(create_call(APILogWorker.drain_all))
111
+ # coroutine so we just schedule the drain in the global loop thread and continue
112
+ from_sync.call_soon_in_loop_thread(create_call(APILogWorker.drain_all))
113
113
  return None
114
114
  else:
115
115
  # We set a timeout of 5s because we don't want to block forever if the worker
@@ -69,7 +69,7 @@ class PrefectLogAdapter(logging.LoggerAdapter):
69
69
 
70
70
 
71
71
  @lru_cache()
72
- def get_logger(name: str = None) -> logging.Logger:
72
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
73
73
  """
74
74
  Get a `prefect` logger. These loggers are intended for internal use within the
75
75
  `prefect` package.
prefect/plugins.py CHANGED
@@ -7,6 +7,7 @@ Currently supported entrypoints:
7
7
  - prefect.collections: Identifies this package as a Prefect collection that
8
8
  should be imported when Prefect is imported.
9
9
  """
10
+
10
11
  import sys
11
12
  from types import ModuleType
12
13
  from typing import Any, Dict, Union
@@ -77,7 +77,7 @@ class CacheKeyFnPolicy(CachePolicy):
77
77
 
78
78
  @dataclass
79
79
  class CompoundCachePolicy(CachePolicy):
80
- policies: list = None
80
+ policies: Optional[list] = None
81
81
 
82
82
  def compute_key(
83
83
  self,
@@ -87,7 +87,7 @@ class CompoundCachePolicy(CachePolicy):
87
87
  **kwargs,
88
88
  ) -> Optional[str]:
89
89
  keys = []
90
- for policy in self.policies:
90
+ for policy in self.policies or []:
91
91
  keys.append(
92
92
  policy.compute_key(
93
93
  task_ctx=task_ctx,
@@ -153,7 +153,7 @@ class Inputs(CachePolicy):
153
153
  And exclude/include config.
154
154
  """
155
155
 
156
- exclude: list = None
156
+ exclude: Optional[list] = None
157
157
 
158
158
  def compute_key(
159
159
  self,
@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any
3
3
 
4
- from prefect.exceptions import ObjectNotFound
4
+ import pendulum
5
+
5
6
  from prefect.results import BaseResult, PersistedResult, ResultFactory
6
7
  from prefect.utilities.asyncutils import run_coro_as_sync
7
8
 
@@ -17,9 +18,15 @@ class ResultFactoryStore(RecordStore):
17
18
  try:
18
19
  result = self.read(key)
19
20
  result.get(_sync=True)
21
+ if result.expiration:
22
+ # if the result has an expiration,
23
+ # check if it is still in the future
24
+ exists = result.expiration > pendulum.now("utc")
25
+ else:
26
+ exists = True
20
27
  self.cache = result
21
- return True
22
- except (ObjectNotFound, ValueError):
28
+ return exists
29
+ except Exception:
23
30
  return False
24
31
 
25
32
  def read(self, key: str) -> BaseResult: