prefect-client 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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 (72) hide show
  1. prefect/_internal/compatibility/migration.py +124 -0
  2. prefect/_internal/concurrency/__init__.py +2 -2
  3. prefect/_internal/concurrency/primitives.py +1 -0
  4. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  5. prefect/_internal/pytz.py +1 -1
  6. prefect/blocks/core.py +1 -1
  7. prefect/blocks/redis.py +168 -0
  8. prefect/client/orchestration.py +113 -23
  9. prefect/client/schemas/actions.py +1 -1
  10. prefect/client/schemas/filters.py +6 -0
  11. prefect/client/schemas/objects.py +22 -11
  12. prefect/client/subscriptions.py +3 -2
  13. prefect/concurrency/asyncio.py +1 -1
  14. prefect/concurrency/services.py +1 -1
  15. prefect/context.py +1 -27
  16. prefect/deployments/__init__.py +3 -0
  17. prefect/deployments/base.py +11 -3
  18. prefect/deployments/deployments.py +3 -0
  19. prefect/deployments/steps/pull.py +1 -0
  20. prefect/deployments/steps/utility.py +2 -1
  21. prefect/engine.py +3 -0
  22. prefect/events/cli/automations.py +1 -1
  23. prefect/events/clients.py +7 -1
  24. prefect/events/schemas/events.py +2 -0
  25. prefect/exceptions.py +9 -0
  26. prefect/filesystems.py +22 -11
  27. prefect/flow_engine.py +118 -156
  28. prefect/flow_runs.py +2 -2
  29. prefect/flows.py +91 -35
  30. prefect/futures.py +44 -43
  31. prefect/infrastructure/provisioners/container_instance.py +1 -0
  32. prefect/infrastructure/provisioners/ecs.py +2 -2
  33. prefect/input/__init__.py +4 -0
  34. prefect/input/run_input.py +4 -2
  35. prefect/logging/formatters.py +2 -2
  36. prefect/logging/handlers.py +2 -2
  37. prefect/logging/loggers.py +1 -1
  38. prefect/plugins.py +1 -0
  39. prefect/records/cache_policies.py +179 -0
  40. prefect/records/result_store.py +10 -3
  41. prefect/results.py +27 -55
  42. prefect/runner/runner.py +1 -1
  43. prefect/runner/server.py +1 -1
  44. prefect/runtime/__init__.py +1 -0
  45. prefect/runtime/deployment.py +1 -0
  46. prefect/runtime/flow_run.py +1 -0
  47. prefect/runtime/task_run.py +1 -0
  48. prefect/settings.py +21 -5
  49. prefect/states.py +17 -4
  50. prefect/task_engine.py +337 -209
  51. prefect/task_runners.py +15 -5
  52. prefect/task_runs.py +203 -0
  53. prefect/{task_server.py → task_worker.py} +66 -36
  54. prefect/tasks.py +180 -77
  55. prefect/transactions.py +92 -16
  56. prefect/types/__init__.py +1 -1
  57. prefect/utilities/asyncutils.py +3 -3
  58. prefect/utilities/callables.py +90 -7
  59. prefect/utilities/dockerutils.py +5 -3
  60. prefect/utilities/engine.py +11 -0
  61. prefect/utilities/filesystem.py +4 -5
  62. prefect/utilities/importtools.py +34 -5
  63. prefect/utilities/services.py +2 -2
  64. prefect/utilities/urls.py +195 -0
  65. prefect/utilities/visualization.py +1 -0
  66. prefect/variables.py +19 -10
  67. prefect/workers/base.py +46 -1
  68. {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/METADATA +3 -2
  69. {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/RECORD +72 -66
  70. {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/LICENSE +0 -0
  71. {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/WHEEL +0 -0
  72. {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/top_level.txt +0 -0
prefect/flow_engine.py CHANGED
@@ -9,6 +9,7 @@ from typing import (
9
9
  Callable,
10
10
  Coroutine,
11
11
  Dict,
12
+ Generator,
12
13
  Generic,
13
14
  Iterable,
14
15
  Literal,
@@ -20,22 +21,17 @@ from typing import (
20
21
  )
21
22
  from uuid import UUID
22
23
 
23
- import anyio
24
- import anyio._backends._asyncio
25
- from sniffio import AsyncLibraryNotFoundError
26
24
  from typing_extensions import ParamSpec
27
25
 
28
- from prefect import Task, get_client
29
- from prefect._internal.concurrency.api import create_call, from_sync
30
- from prefect.client.orchestration import SyncPrefectClient
26
+ from prefect import Task
27
+ from prefect.client.orchestration import SyncPrefectClient, get_client
31
28
  from prefect.client.schemas import FlowRun, TaskRun
32
29
  from prefect.client.schemas.filters import FlowRunFilter
33
30
  from prefect.client.schemas.sorting import FlowRunSort
34
- from prefect.context import ClientContext, FlowRunContext, TagsContext, TaskRunContext
31
+ from prefect.context import ClientContext, FlowRunContext, TagsContext
35
32
  from prefect.exceptions import Abort, Pause, PrefectException, UpstreamTaskError
36
33
  from prefect.flows import Flow, load_flow_from_entrypoint, load_flow_from_flow_run
37
34
  from prefect.futures import PrefectFuture, resolve_futures_to_states
38
- from prefect.logging.handlers import APILogHandler
39
35
  from prefect.logging.loggers import (
40
36
  flow_run_logger,
41
37
  get_logger,
@@ -43,7 +39,7 @@ from prefect.logging.loggers import (
43
39
  patch_print,
44
40
  )
45
41
  from prefect.results import ResultFactory
46
- from prefect.settings import PREFECT_DEBUG_MODE, PREFECT_UI_URL
42
+ from prefect.settings import PREFECT_DEBUG_MODE
47
43
  from prefect.states import (
48
44
  Failed,
49
45
  Pending,
@@ -54,7 +50,7 @@ from prefect.states import (
54
50
  return_value_to_state,
55
51
  )
56
52
  from prefect.utilities.asyncutils import run_coro_as_sync
57
- from prefect.utilities.callables import parameters_to_args_kwargs
53
+ from prefect.utilities.callables import call_with_parameters
58
54
  from prefect.utilities.collections import visit_collection
59
55
  from prefect.utilities.engine import (
60
56
  _get_hook_name,
@@ -64,6 +60,7 @@ from prefect.utilities.engine import (
64
60
  resolve_to_final_result,
65
61
  )
66
62
  from prefect.utilities.timeout import timeout, timeout_async
63
+ from prefect.utilities.urls import url_for
67
64
 
68
65
  P = ParamSpec("P")
69
66
  R = TypeVar("R")
@@ -174,9 +171,6 @@ class FlowRunEngine(Generic[P, R]):
174
171
  while state.is_pending():
175
172
  time.sleep(0.2)
176
173
  state = self.set_state(new_state)
177
- if state.is_running():
178
- for hook in self.get_hooks(state):
179
- hook()
180
174
  return state
181
175
 
182
176
  def set_state(self, state: State, force: bool = False) -> State:
@@ -349,12 +343,14 @@ class FlowRunEngine(Generic[P, R]):
349
343
 
350
344
  return flow_run
351
345
 
352
- def get_hooks(self, state: State, as_async: bool = False) -> Iterable[Callable]:
346
+ def call_hooks(self, state: State = None) -> Iterable[Callable]:
347
+ if state is None:
348
+ state = self.state
353
349
  flow = self.flow
354
350
  flow_run = self.flow_run
355
351
 
356
352
  if not flow_run:
357
- raise ValueError("Task run is not set")
353
+ raise ValueError("Flow run is not set")
358
354
 
359
355
  enable_cancellation_and_crashed_hooks = (
360
356
  os.environ.get(
@@ -363,7 +359,6 @@ class FlowRunEngine(Generic[P, R]):
363
359
  == "true"
364
360
  )
365
361
 
366
- hooks = None
367
362
  if state.is_failed() and flow.on_failure_hooks:
368
363
  hooks = flow.on_failure_hooks
369
364
  elif state.is_completed() and flow.on_completion_hooks:
@@ -382,48 +377,30 @@ class FlowRunEngine(Generic[P, R]):
382
377
  hooks = flow.on_crashed_hooks
383
378
  elif state.is_running() and flow.on_running_hooks:
384
379
  hooks = flow.on_running_hooks
380
+ else:
381
+ hooks = None
385
382
 
386
383
  for hook in hooks or []:
387
384
  hook_name = _get_hook_name(hook)
388
385
 
389
- @contextmanager
390
- def hook_context():
391
- try:
392
- self.logger.info(
393
- f"Running hook {hook_name!r} in response to entering state"
394
- f" {state.name!r}"
395
- )
396
- yield
397
- except Exception:
398
- self.logger.error(
399
- f"An error was encountered while running hook {hook_name!r}",
400
- exc_info=True,
401
- )
402
- else:
403
- self.logger.info(
404
- f"Hook {hook_name!r} finished running successfully"
405
- )
406
-
407
- if as_async:
408
-
409
- async def _hook_fn():
410
- with hook_context():
411
- result = hook(flow, flow_run, state)
412
- if inspect.isawaitable(result):
413
- await result
414
-
386
+ try:
387
+ self.logger.info(
388
+ f"Running hook {hook_name!r} in response to entering state"
389
+ f" {state.name!r}"
390
+ )
391
+ result = hook(flow, flow_run, state)
392
+ if inspect.isawaitable(result):
393
+ run_coro_as_sync(result)
394
+ except Exception:
395
+ self.logger.error(
396
+ f"An error was encountered while running hook {hook_name!r}",
397
+ exc_info=True,
398
+ )
415
399
  else:
416
-
417
- def _hook_fn():
418
- with hook_context():
419
- result = hook(flow, flow_run, state)
420
- if inspect.isawaitable(result):
421
- run_coro_as_sync(result)
422
-
423
- yield _hook_fn
400
+ self.logger.info(f"Hook {hook_name!r} finished running successfully")
424
401
 
425
402
  @contextmanager
426
- def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
403
+ def setup_run_context(self, client: Optional[SyncPrefectClient] = None):
427
404
  from prefect.utilities.engine import (
428
405
  should_log_prints,
429
406
  )
@@ -436,13 +413,6 @@ class FlowRunEngine(Generic[P, R]):
436
413
  self.flow_run = client.read_flow_run(self.flow_run.id)
437
414
  log_prints = should_log_prints(self.flow)
438
415
 
439
- # if running in a completely synchronous frame, anyio will not detect the
440
- # backend to use for the task group
441
- try:
442
- task_group = anyio.create_task_group()
443
- except AsyncLibraryNotFoundError:
444
- task_group = anyio._backends._asyncio.TaskGroup()
445
-
446
416
  with ExitStack() as stack:
447
417
  # TODO: Explore closing task runner before completing the flow to
448
418
  # wait for futures to complete
@@ -457,7 +427,6 @@ class FlowRunEngine(Generic[P, R]):
457
427
  flow_run=self.flow_run,
458
428
  parameters=self.parameters,
459
429
  client=client,
460
- background_tasks=task_group,
461
430
  result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
462
431
  task_runner=task_runner,
463
432
  )
@@ -482,7 +451,7 @@ class FlowRunEngine(Generic[P, R]):
482
451
  yield
483
452
 
484
453
  @contextmanager
485
- def start(self):
454
+ def initialize_run(self):
486
455
  """
487
456
  Enters a client context and creates a flow run if needed.
488
457
  """
@@ -490,27 +459,29 @@ class FlowRunEngine(Generic[P, R]):
490
459
  self._client = client_ctx.sync_client
491
460
  self._is_started = True
492
461
 
493
- # this conditional is engaged whenever a run is triggered via deployment
494
- if self.flow_run_id and not self.flow:
495
- self.flow_run = self.client.read_flow_run(self.flow_run_id)
496
- try:
497
- self.flow = self.load_flow(self.client)
498
- except Exception as exc:
499
- self.handle_exception(
500
- exc,
501
- msg="Failed to load flow from entrypoint.",
502
- )
503
- self.short_circuit = True
504
-
505
462
  if not self.flow_run:
506
463
  self.flow_run = self.create_flow_run(self.client)
464
+ flow_run_url = url_for(self.flow_run)
507
465
 
508
- ui_url = PREFECT_UI_URL.value()
509
- if ui_url:
466
+ if flow_run_url:
510
467
  self.logger.info(
511
- f"View at {ui_url}/flow-runs/flow-run/{self.flow_run.id}",
512
- extra={"send_to_api": False},
468
+ f"View at {flow_run_url}", extra={"send_to_api": False}
513
469
  )
470
+ else:
471
+ # Update the empirical policy to match the flow if it is not set
472
+ if self.flow_run.empirical_policy.retry_delay is None:
473
+ self.flow_run.empirical_policy.retry_delay = (
474
+ self.flow.retry_delay_seconds
475
+ )
476
+
477
+ if self.flow_run.empirical_policy.retries is None:
478
+ self.flow_run.empirical_policy.retries = self.flow.retries
479
+
480
+ self.client.update_flow_run(
481
+ flow_run_id=self.flow_run.id,
482
+ flow_version=self.flow.version,
483
+ empirical_policy=self.flow_run.empirical_policy,
484
+ )
514
485
 
515
486
  # validate prior to context so that context receives validated params
516
487
  if self.flow.should_validate_parameters:
@@ -536,6 +507,9 @@ class FlowRunEngine(Generic[P, R]):
536
507
  raise
537
508
  except (Abort, Pause):
538
509
  raise
510
+ except GeneratorExit:
511
+ # Do not capture generator exits as crashes
512
+ raise
539
513
  except BaseException as exc:
540
514
  # BaseExceptions are caught and handled as crashes
541
515
  self.handle_crash(exc)
@@ -550,12 +524,6 @@ class FlowRunEngine(Generic[P, R]):
550
524
  msg=f"Finished in state {display_state}",
551
525
  )
552
526
 
553
- # flush any logs in the background if this is a "top" level run
554
- if not (FlowRunContext.get() or TaskRunContext.get()):
555
- from_sync.call_soon_in_loop_thread(
556
- create_call(APILogHandler.aflush)
557
- )
558
-
559
527
  self._is_started = False
560
528
  self._client = None
561
529
 
@@ -569,61 +537,81 @@ class FlowRunEngine(Generic[P, R]):
569
537
  return False # TODO: handle this differently?
570
538
  return getattr(self, "flow_run").state.is_pending()
571
539
 
540
+ # --------------------------
541
+ #
542
+ # The following methods compose the main task run loop
543
+ #
544
+ # --------------------------
572
545
 
573
- async def run_flow_async(
574
- flow: Flow[P, Coroutine[Any, Any, R]],
546
+ @contextmanager
547
+ def start(self) -> Generator[None, None, None]:
548
+ with self.initialize_run():
549
+ self.begin_run()
550
+
551
+ if self.state.is_running():
552
+ self.call_hooks()
553
+ try:
554
+ yield
555
+ finally:
556
+ if self.state.is_final() or self.state.is_cancelling():
557
+ self.call_hooks()
558
+
559
+ @contextmanager
560
+ def run_context(self):
561
+ timeout_context = timeout_async if self.flow.isasync else timeout
562
+ # reenter the run context to ensure it is up to date for every run
563
+ with self.setup_run_context():
564
+ try:
565
+ with timeout_context(seconds=self.flow.timeout_seconds):
566
+ self.logger.debug(
567
+ f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
568
+ )
569
+ yield self
570
+ except TimeoutError as exc:
571
+ self.handle_timeout(exc)
572
+ except Exception as exc:
573
+ self.logger.exception(f"Encountered exception during execution: {exc}")
574
+ self.handle_exception(exc)
575
+
576
+ def call_flow_fn(self) -> Union[R, Coroutine[Any, Any, R]]:
577
+ """
578
+ Convenience method to call the flow function. Returns a coroutine if the
579
+ flow is async.
580
+ """
581
+ if self.flow.isasync:
582
+
583
+ async def _call_flow_fn():
584
+ result = await call_with_parameters(self.flow.fn, self.parameters)
585
+ self.handle_success(result)
586
+
587
+ return _call_flow_fn()
588
+ else:
589
+ result = call_with_parameters(self.flow.fn, self.parameters)
590
+ self.handle_success(result)
591
+
592
+
593
+ def run_flow_sync(
594
+ flow: Flow[P, R],
575
595
  flow_run: Optional[FlowRun] = None,
576
596
  parameters: Optional[Dict[str, Any]] = None,
577
597
  wait_for: Optional[Iterable[PrefectFuture]] = None,
578
598
  return_type: Literal["state", "result"] = "result",
579
- ) -> Union[R, None]:
580
- """
581
- Runs a flow against the API.
599
+ ) -> Union[R, State, None]:
600
+ parameters = flow_run.parameters if flow_run else parameters
582
601
 
583
- We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
584
- """
585
602
  engine = FlowRunEngine[P, R](
586
- flow=flow,
587
- parameters=flow_run.parameters if flow_run else parameters,
588
- flow_run=flow_run,
589
- wait_for=wait_for,
603
+ flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
590
604
  )
591
605
 
592
- # This is a context manager that keeps track of the state of the flow run.
593
- with engine.start() as run:
594
- run.begin_run()
595
-
596
- while run.is_running():
597
- with run.enter_run_context():
598
- try:
599
- # This is where the flow is actually run.
600
- with timeout_async(seconds=run.flow.timeout_seconds):
601
- call_args, call_kwargs = parameters_to_args_kwargs(
602
- flow.fn, run.parameters or {}
603
- )
604
- run.logger.debug(
605
- f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
606
- )
607
- result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
608
- # If the flow run is successful, finalize it.
609
- run.handle_success(result)
610
-
611
- except TimeoutError as exc:
612
- run.handle_timeout(exc)
613
- except Exception as exc:
614
- # If the flow fails, and we have retries left, set the flow to retrying.
615
- run.logger.exception("Encountered exception during execution:")
616
- run.handle_exception(exc)
606
+ with engine.start():
607
+ while engine.is_running():
608
+ with engine.run_context():
609
+ engine.call_flow_fn()
617
610
 
618
- if run.state.is_final() or run.state.is_cancelling():
619
- for hook in run.get_hooks(run.state, as_async=True):
620
- await hook()
621
- if return_type == "state":
622
- return run.state
623
- return run.result()
611
+ return engine.state if return_type == "state" else engine.result()
624
612
 
625
613
 
626
- def run_flow_sync(
614
+ async def run_flow_async(
627
615
  flow: Flow[P, R],
628
616
  flow_run: Optional[FlowRun] = None,
629
617
  parameters: Optional[Dict[str, Any]] = None,
@@ -636,38 +624,12 @@ def run_flow_sync(
636
624
  flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
637
625
  )
638
626
 
639
- # This is a context manager that keeps track of the state of the flow run.
640
- with engine.start() as run:
641
- run.begin_run()
627
+ with engine.start():
628
+ while engine.is_running():
629
+ with engine.run_context():
630
+ await engine.call_flow_fn()
642
631
 
643
- while run.is_running():
644
- with run.enter_run_context():
645
- try:
646
- # This is where the flow is actually run.
647
- with timeout(seconds=run.flow.timeout_seconds):
648
- call_args, call_kwargs = parameters_to_args_kwargs(
649
- flow.fn, run.parameters or {}
650
- )
651
- run.logger.debug(
652
- f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
653
- )
654
- result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
655
- # If the flow run is successful, finalize it.
656
- run.handle_success(result)
657
-
658
- except TimeoutError as exc:
659
- run.handle_timeout(exc)
660
- except Exception as exc:
661
- # If the flow fails, and we have retries left, set the flow to retrying.
662
- run.logger.exception("Encountered exception during execution:")
663
- run.handle_exception(exc)
664
-
665
- if run.state.is_final() or run.state.is_cancelling():
666
- for hook in run.get_hooks(run.state):
667
- hook()
668
- if return_type == "state":
669
- return run.state
670
- return run.result()
632
+ return engine.state if return_type == "state" else engine.result()
671
633
 
672
634
 
673
635
  def run_flow(
prefect/flow_runs.py CHANGED
@@ -76,7 +76,7 @@ async def wait_for_flow_run(
76
76
  ```python
77
77
  import asyncio
78
78
 
79
- from prefect import get_client
79
+ from prefect.client.orchestration import get_client
80
80
  from prefect.flow_runs import wait_for_flow_run
81
81
 
82
82
  async def main():
@@ -94,7 +94,7 @@ async def wait_for_flow_run(
94
94
  ```python
95
95
  import asyncio
96
96
 
97
- from prefect import get_client
97
+ from prefect.client.orchestration import get_client
98
98
  from prefect.flow_runs import wait_for_flow_run
99
99
 
100
100
  async def main(num_runs: int):