prefect-client 3.2.5__py3-none-any.whl → 3.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prefect/_build_info.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.2.5"
3
- __build_date__ = "2025-02-19 03:24:41.213047+00:00"
4
- __git_commit__ = "168280f75a7c7cbd871a0cfbbde8a8bfc1834026"
2
+ __version__ = "3.2.7"
3
+ __build_date__ = "2025-02-21 19:38:38.599741+00:00"
4
+ __git_commit__ = "d4d9001e40eb378ec68a97f7e3bab07d89c7557c"
5
5
  __dirty__ = False
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import gzip
6
+ import multiprocessing
7
+ import multiprocessing.context
8
+ import os
9
+ from typing import Any, TypedDict
10
+
11
+ import cloudpickle
12
+
13
+ from prefect.client.schemas.objects import FlowRun
14
+ from prefect.context import SettingsContext, get_settings_context, serialize_context
15
+ from prefect.engine import handle_engine_signals
16
+ from prefect.flow_engine import run_flow
17
+ from prefect.flows import Flow
18
+ from prefect.settings.context import get_current_settings
19
+ from prefect.settings.models.root import Settings
20
+
21
+
22
+ class SerializedBundle(TypedDict):
23
+ """
24
+ A serialized bundle is a serialized function, context, and flow run that can be
25
+ easily transported for later execution.
26
+ """
27
+
28
+ function: str
29
+ context: str
30
+ flow_run: dict[str, Any]
31
+
32
+
33
+ def _serialize_bundle_object(obj: Any) -> str:
34
+ """
35
+ Serializes an object to a string.
36
+ """
37
+ return base64.b64encode(gzip.compress(cloudpickle.dumps(obj))).decode()
38
+
39
+
40
+ def _deserialize_bundle_object(serialized_obj: str) -> Any:
41
+ """
42
+ Deserializes an object from a string.
43
+ """
44
+ return cloudpickle.loads(gzip.decompress(base64.b64decode(serialized_obj)))
45
+
46
+
47
+ def create_bundle_for_flow_run(
48
+ flow: Flow[Any, Any],
49
+ flow_run: FlowRun,
50
+ context: dict[str, Any] | None = None,
51
+ ) -> SerializedBundle:
52
+ """
53
+ Creates a bundle for a flow run.
54
+
55
+ Args:
56
+ flow: The flow to bundle.
57
+ flow_run: The flow run to bundle.
58
+ context: The context to use when running the flow.
59
+
60
+ Returns:
61
+ A serialized bundle.
62
+ """
63
+ context = context or serialize_context()
64
+
65
+ return {
66
+ "function": _serialize_bundle_object(flow),
67
+ "context": _serialize_bundle_object(context),
68
+ "flow_run": flow_run.model_dump(mode="json"),
69
+ }
70
+
71
+
72
+ def extract_flow_from_bundle(bundle: SerializedBundle) -> Flow[Any, Any]:
73
+ """
74
+ Extracts a flow from a bundle.
75
+ """
76
+ return _deserialize_bundle_object(bundle["function"])
77
+
78
+
79
+ def _extract_and_run_flow(
80
+ bundle: SerializedBundle, env: dict[str, Any] | None = None
81
+ ) -> None:
82
+ """
83
+ Extracts a flow from a bundle and runs it.
84
+
85
+ Designed to be run in a subprocess.
86
+
87
+ Args:
88
+ bundle: The bundle to extract and run.
89
+ env: The environment to use when running the flow.
90
+ """
91
+
92
+ os.environ.update(env or {})
93
+ # TODO: make this a thing we can pass directly to the engine
94
+ os.environ["PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS"] = "false"
95
+ settings_context = get_settings_context()
96
+
97
+ flow = _deserialize_bundle_object(bundle["function"])
98
+ context = _deserialize_bundle_object(bundle["context"])
99
+ flow_run = FlowRun.model_validate(bundle["flow_run"])
100
+
101
+ with SettingsContext(
102
+ profile=settings_context.profile,
103
+ settings=Settings(),
104
+ ):
105
+ with handle_engine_signals(flow_run.id):
106
+ maybe_coro = run_flow(
107
+ flow=flow,
108
+ flow_run=flow_run,
109
+ context=context,
110
+ )
111
+ if asyncio.iscoroutine(maybe_coro):
112
+ # This is running in a brand new process, so there won't be an existing
113
+ # event loop.
114
+ asyncio.run(maybe_coro)
115
+
116
+
117
+ def execute_bundle_in_subprocess(
118
+ bundle: SerializedBundle,
119
+ ) -> multiprocessing.context.SpawnProcess:
120
+ """
121
+ Executes a bundle in a subprocess.
122
+
123
+ Args:
124
+ bundle: The bundle to execute.
125
+
126
+ Returns:
127
+ A multiprocessing.context.SpawnProcess.
128
+ """
129
+
130
+ ctx = multiprocessing.get_context("spawn")
131
+
132
+ process = ctx.Process(
133
+ target=_extract_and_run_flow,
134
+ kwargs={
135
+ "bundle": bundle,
136
+ "env": get_current_settings().to_environment_variables(exclude_unset=True)
137
+ | os.environ,
138
+ },
139
+ )
140
+
141
+ process.start()
142
+
143
+ return process
@@ -81,6 +81,7 @@ from prefect.client.orchestration._blocks_types.client import (
81
81
 
82
82
  import prefect
83
83
  import prefect.exceptions
84
+ from prefect.logging.loggers import get_run_logger
84
85
  import prefect.settings
85
86
  import prefect.states
86
87
  from prefect.client.constants import SERVER_API_VERSION
@@ -1182,8 +1183,17 @@ class PrefectClient(
1182
1183
  if api_version.major != client_version.major:
1183
1184
  raise RuntimeError(
1184
1185
  f"Found incompatible versions: client: {client_version}, server: {api_version}. "
1185
- f"Major versions must match."
1186
+ "Major versions must match."
1186
1187
  )
1188
+ if api_version < client_version:
1189
+ warning_message = (
1190
+ "Your Prefect server is running an older version of Prefect than your client which may result in unexpected behavior. "
1191
+ f"Please upgrade your Prefect server from version {api_version} to version {client_version} or higher."
1192
+ )
1193
+ try:
1194
+ get_run_logger().warning(warning_message)
1195
+ except prefect.context.MissingContextError:
1196
+ self.logger.warning(warning_message)
1187
1197
 
1188
1198
  async def __aenter__(self) -> Self:
1189
1199
  """
@@ -1523,8 +1533,17 @@ class SyncPrefectClient(
1523
1533
  if api_version.major != client_version.major:
1524
1534
  raise RuntimeError(
1525
1535
  f"Found incompatible versions: client: {client_version}, server: {api_version}. "
1526
- f"Major versions must match."
1536
+ "Major versions must match."
1527
1537
  )
1538
+ if api_version < client_version:
1539
+ warning_message = (
1540
+ "Your Prefect server is running an older version of Prefect than your client which may result in unexpected behavior. "
1541
+ f"Please upgrade your Prefect server from version {api_version} to version {client_version} or higher."
1542
+ )
1543
+ try:
1544
+ get_run_logger().warning(warning_message)
1545
+ except prefect.context.MissingContextError:
1546
+ self.logger.warning(warning_message)
1528
1547
 
1529
1548
  def set_task_run_name(self, task_run_id: UUID, name: str) -> httpx.Response:
1530
1549
  task_run_data = TaskRunUpdate(name=name)
prefect/engine.py CHANGED
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import sys
5
+ from contextlib import contextmanager
3
6
  from typing import TYPE_CHECKING, Any, Callable
4
7
  from uuid import UUID
5
8
 
@@ -18,13 +21,71 @@ from prefect.utilities.asyncutils import (
18
21
  if TYPE_CHECKING:
19
22
  import logging
20
23
 
21
- from prefect.flow_engine import FlowRun
24
+ from prefect.client.schemas.objects import FlowRun
22
25
  from prefect.flows import Flow
23
26
  from prefect.logging.loggers import LoggingAdapter
24
27
 
25
28
  engine_logger: "logging.Logger" = get_logger("engine")
26
29
 
27
30
 
31
+ @contextmanager
32
+ def handle_engine_signals(flow_run_id: UUID | None = None):
33
+ """
34
+ Handle signals from the orchestrator to abort or pause the flow run or otherwise
35
+ handle unexpected exceptions.
36
+
37
+ This context manager will handle exiting the process depending on the signal received.
38
+
39
+ Args:
40
+ flow_run_id: The ID of the flow run to handle signals for.
41
+
42
+ Example:
43
+ ```python
44
+ from prefect import flow
45
+ from prefect.engine import handle_engine_signals
46
+ from prefect.flow_engine import run_flow
47
+
48
+ @flow
49
+ def my_flow():
50
+ print("Hello, world!")
51
+
52
+ with handle_engine_signals():
53
+ run_flow(my_flow)
54
+ ```
55
+ """
56
+ try:
57
+ yield
58
+ except Abort:
59
+ if flow_run_id:
60
+ msg = f"Execution of flow run '{flow_run_id}' aborted by orchestrator."
61
+ else:
62
+ msg = "Execution aborted by orchestrator."
63
+ engine_logger.info(msg)
64
+ exit(0)
65
+ except Pause:
66
+ if flow_run_id:
67
+ msg = f"Execution of flow run '{flow_run_id}' is paused."
68
+ else:
69
+ msg = "Execution is paused."
70
+ engine_logger.info(msg)
71
+ exit(0)
72
+ except Exception:
73
+ if flow_run_id:
74
+ msg = f"Execution of flow run '{flow_run_id}' exited with unexpected exception"
75
+ else:
76
+ msg = "Execution exited with unexpected exception"
77
+ engine_logger.error(msg, exc_info=True)
78
+ exit(1)
79
+ except BaseException:
80
+ if flow_run_id:
81
+ msg = f"Execution of flow run '{flow_run_id}' interrupted by base exception"
82
+ else:
83
+ msg = "Execution interrupted by base exception"
84
+ engine_logger.error(msg, exc_info=True)
85
+ # Let the exit code be determined by the base exception type
86
+ raise
87
+
88
+
28
89
  if __name__ == "__main__":
29
90
  try:
30
91
  flow_run_id: UUID = UUID(
@@ -36,7 +97,7 @@ if __name__ == "__main__":
36
97
  )
37
98
  exit(1)
38
99
 
39
- try:
100
+ with handle_engine_signals(flow_run_id):
40
101
  from prefect.flow_engine import (
41
102
  flow_run_logger,
42
103
  load_flow,
@@ -62,32 +123,5 @@ if __name__ == "__main__":
62
123
  else:
63
124
  run_flow(flow, flow_run=flow_run, error_logger=run_logger)
64
125
 
65
- except Abort:
66
- engine_logger.info(
67
- f"Engine execution of flow run '{flow_run_id}' aborted by orchestrator."
68
- )
69
- exit(0)
70
- except Pause:
71
- engine_logger.info(f"Engine execution of flow run '{flow_run_id}' is paused.")
72
- exit(0)
73
- except Exception:
74
- engine_logger.error(
75
- (
76
- f"Engine execution of flow run '{flow_run_id}' exited with unexpected "
77
- "exception"
78
- ),
79
- exc_info=True,
80
- )
81
- exit(1)
82
- except BaseException:
83
- engine_logger.error(
84
- (
85
- f"Engine execution of flow run '{flow_run_id}' interrupted by base "
86
- "exception"
87
- ),
88
- exc_info=True,
89
- )
90
- # Let the exit code be determined by the base exception type
91
- raise
92
126
 
93
127
  __getattr__: Callable[[str], Any] = getattr_migration(__name__)
@@ -84,7 +84,7 @@ class Trigger(PrefectBaseModel, abc.ABC, extra="ignore"): # type: ignore[call-a
84
84
  getattr(self, "name", None)
85
85
  or f"Automation for deployment {self._deployment_id}"
86
86
  ),
87
- description="",
87
+ description=getattr(self, "description", ""),
88
88
  enabled=getattr(self, "enabled", True),
89
89
  trigger=trigger,
90
90
  actions=self.actions(),
prefect/flow_engine.py CHANGED
@@ -47,6 +47,7 @@ from prefect.context import (
47
47
  hydrated_context,
48
48
  serialize_context,
49
49
  )
50
+ from prefect.engine import handle_engine_signals
50
51
  from prefect.exceptions import (
51
52
  Abort,
52
53
  MissingFlowError,
@@ -1592,8 +1593,6 @@ def run_flow_in_subprocess(
1592
1593
  """
1593
1594
  Wrapper function to update environment variables and settings before running the flow.
1594
1595
  """
1595
- engine_logger = logging.getLogger("prefect.engine")
1596
-
1597
1596
  os.environ.update(env or {})
1598
1597
  settings_context = get_settings_context()
1599
1598
  # Create a new settings context with a new settings object to pick up the updated
@@ -1602,41 +1601,12 @@ def run_flow_in_subprocess(
1602
1601
  profile=settings_context.profile,
1603
1602
  settings=Settings(),
1604
1603
  ):
1605
- try:
1604
+ with handle_engine_signals(getattr(flow_run, "id", None)):
1606
1605
  maybe_coro = run_flow(*args, **kwargs)
1607
1606
  if asyncio.iscoroutine(maybe_coro):
1608
1607
  # This is running in a brand new process, so there won't be an existing
1609
1608
  # event loop.
1610
1609
  asyncio.run(maybe_coro)
1611
- except Abort:
1612
- if flow_run:
1613
- msg = f"Execution of flow run '{flow_run.id}' aborted by orchestrator."
1614
- else:
1615
- msg = "Execution aborted by orchestrator."
1616
- engine_logger.info(msg)
1617
- exit(0)
1618
- except Pause:
1619
- if flow_run:
1620
- msg = f"Execution of flow run '{flow_run.id}' is paused."
1621
- else:
1622
- msg = "Execution is paused."
1623
- engine_logger.info(msg)
1624
- exit(0)
1625
- except Exception:
1626
- if flow_run:
1627
- msg = f"Execution of flow run '{flow_run.id}' exited with unexpected exception"
1628
- else:
1629
- msg = "Execution exited with unexpected exception"
1630
- engine_logger.error(msg, exc_info=True)
1631
- exit(1)
1632
- except BaseException:
1633
- if flow_run:
1634
- msg = f"Execution of flow run '{flow_run.id}' interrupted by base exception"
1635
- else:
1636
- msg = "Execution interrupted by base exception"
1637
- engine_logger.error(msg, exc_info=True)
1638
- # Let the exit code be determined by the base exception type
1639
- raise
1640
1610
 
1641
1611
  ctx = multiprocessing.get_context("spawn")
1642
1612
 
prefect/flows.py CHANGED
@@ -2350,9 +2350,15 @@ async def load_flow_from_flow_run(
2350
2350
  f"Running {len(deployment.pull_steps)} deployment pull step(s)"
2351
2351
  )
2352
2352
 
2353
- from prefect.deployments.steps.core import run_steps
2353
+ from prefect.deployments.steps.core import StepExecutionError, run_steps
2354
+
2355
+ try:
2356
+ output = await run_steps(deployment.pull_steps)
2357
+ except StepExecutionError as e:
2358
+ e = e.__cause__ or e
2359
+ run_logger.error(str(e))
2360
+ raise
2354
2361
 
2355
- output = await run_steps(deployment.pull_steps)
2356
2362
  if output.get("directory"):
2357
2363
  run_logger.debug(f"Changing working directory to {output['directory']!r}")
2358
2364
  os.chdir(output["directory"])
@@ -38,7 +38,9 @@ def load_logging_config(path: Path) -> dict[str, Any]:
38
38
  warnings.filterwarnings("ignore", category=DeprecationWarning)
39
39
  config = yaml.safe_load(
40
40
  # Substitute settings into the template in format $SETTING / ${SETTING}
41
- template.substitute(current_settings.to_environment_variables())
41
+ template.substitute(
42
+ current_settings.to_environment_variables(include_aliases=True)
43
+ )
42
44
  )
43
45
 
44
46
  # Load overrides from the environment
prefect/runner/runner.py CHANGED
@@ -64,9 +64,15 @@ from uuid import UUID, uuid4
64
64
 
65
65
  import anyio
66
66
  import anyio.abc
67
+ import anyio.to_thread
67
68
  from cachetools import LRUCache
68
69
  from typing_extensions import Self
69
70
 
71
+ from prefect._experimental.bundles import (
72
+ SerializedBundle,
73
+ execute_bundle_in_subprocess,
74
+ extract_flow_from_bundle,
75
+ )
70
76
  from prefect._internal.concurrency.api import (
71
77
  create_call,
72
78
  from_async,
@@ -135,7 +141,7 @@ __all__ = ["Runner"]
135
141
 
136
142
 
137
143
  class ProcessMapEntry(TypedDict):
138
- flow_run: FlowRun
144
+ flow_run: "FlowRun"
139
145
  pid: int
140
146
 
141
147
 
@@ -221,6 +227,7 @@ class Runner:
221
227
  self._scheduled_task_scopes: set[anyio.abc.CancelScope] = set()
222
228
  self._deployment_ids: set[UUID] = set()
223
229
  self._flow_run_process_map: dict[UUID, ProcessMapEntry] = dict()
230
+ self._flow_run_bundle_map: dict[UUID, SerializedBundle] = dict()
224
231
 
225
232
  self._tmp_dir: Path = (
226
233
  Path(tempfile.gettempdir()) / "runner_storage" / str(uuid4())
@@ -508,7 +515,7 @@ class Runner:
508
515
  return asyncio.run_coroutine_threadsafe(func(*args, **kwargs), self._loop)
509
516
 
510
517
  async def cancel_all(self) -> None:
511
- runs_to_cancel: list[FlowRun] = []
518
+ runs_to_cancel: list["FlowRun"] = []
512
519
 
513
520
  # done to avoid dictionary size changing during iteration
514
521
  for info in self._flow_run_process_map.values():
@@ -602,7 +609,120 @@ class Runner:
602
609
  )
603
610
  )
604
611
 
605
- def _get_flow_run_logger(self, flow_run: "FlowRun | FlowRun") -> PrefectLogAdapter:
612
+ async def execute_bundle(self, bundle: SerializedBundle) -> None:
613
+ """
614
+ Executes a bundle in a subprocess.
615
+ """
616
+ from prefect.client.schemas.objects import FlowRun
617
+
618
+ self.pause_on_shutdown = False
619
+ context = self if not self.started else asyncnullcontext()
620
+
621
+ flow_run = FlowRun.model_validate(bundle["flow_run"])
622
+
623
+ async with context:
624
+ if not self._acquire_limit_slot(flow_run.id):
625
+ return
626
+
627
+ process = execute_bundle_in_subprocess(bundle)
628
+
629
+ if process.pid is None:
630
+ # This shouldn't happen because `execute_bundle_in_subprocess` starts the process
631
+ # but we'll handle it gracefully anyway
632
+ msg = "Failed to start process for flow execution. No PID returned."
633
+ await self._propose_crashed_state(flow_run, msg)
634
+ raise RuntimeError(msg)
635
+
636
+ self._flow_run_process_map[flow_run.id] = ProcessMapEntry(
637
+ pid=process.pid, flow_run=flow_run
638
+ )
639
+ self._flow_run_bundle_map[flow_run.id] = bundle
640
+
641
+ tasks: list[asyncio.Task[None]] = []
642
+ tasks.append(
643
+ asyncio.create_task(
644
+ critical_service_loop(
645
+ workload=self._check_for_cancelled_flow_runs,
646
+ interval=self.query_seconds,
647
+ jitter_range=0.3,
648
+ )
649
+ )
650
+ )
651
+ if self.heartbeat_seconds is not None:
652
+ tasks.append(
653
+ asyncio.create_task(
654
+ critical_service_loop(
655
+ workload=self._emit_flow_run_heartbeats,
656
+ interval=self.heartbeat_seconds,
657
+ jitter_range=0.1,
658
+ )
659
+ )
660
+ )
661
+
662
+ await anyio.to_thread.run_sync(process.join)
663
+
664
+ for task in tasks:
665
+ task.cancel()
666
+
667
+ await asyncio.gather(*tasks, return_exceptions=True)
668
+
669
+ self._flow_run_process_map.pop(flow_run.id)
670
+
671
+ flow_run_logger = self._get_flow_run_logger(flow_run)
672
+ if process.exitcode is None:
673
+ raise RuntimeError("Process has no exit code")
674
+
675
+ if process.exitcode:
676
+ help_message = None
677
+ level = logging.ERROR
678
+ if process.exitcode == -9:
679
+ level = logging.INFO
680
+ help_message = (
681
+ "This indicates that the process exited due to a SIGKILL signal. "
682
+ "Typically, this is either caused by manual cancellation or "
683
+ "high memory usage causing the operating system to "
684
+ "terminate the process."
685
+ )
686
+ if process.exitcode == -15:
687
+ level = logging.INFO
688
+ help_message = (
689
+ "This indicates that the process exited due to a SIGTERM signal. "
690
+ "Typically, this is caused by manual cancellation."
691
+ )
692
+ elif process.exitcode == 247:
693
+ help_message = (
694
+ "This indicates that the process was terminated due to high "
695
+ "memory usage."
696
+ )
697
+ elif (
698
+ sys.platform == "win32"
699
+ and process.returncode == STATUS_CONTROL_C_EXIT
700
+ ):
701
+ level = logging.INFO
702
+ help_message = (
703
+ "Process was terminated due to a Ctrl+C or Ctrl+Break signal. "
704
+ "Typically, this is caused by manual cancellation."
705
+ )
706
+
707
+ flow_run_logger.log(
708
+ level,
709
+ f"Process for flow run {flow_run.name!r} exited with status code:"
710
+ f" {process.exitcode}"
711
+ + (f"; {help_message}" if help_message else ""),
712
+ )
713
+ terminal_state = await self._propose_crashed_state(
714
+ flow_run, help_message or "Process exited with non-zero exit code"
715
+ )
716
+ if terminal_state:
717
+ await self._run_on_crashed_hooks(
718
+ flow_run=flow_run, state=terminal_state
719
+ )
720
+ else:
721
+ flow_run_logger.info(
722
+ f"Process for flow run {flow_run.name!r} exited cleanly."
723
+ )
724
+
725
+ def _get_flow_run_logger(self, flow_run: "FlowRun") -> PrefectLogAdapter:
606
726
  return flow_run_logger(flow_run=flow_run).getChild(
607
727
  "runner",
608
728
  extra={
@@ -1308,8 +1428,11 @@ class Runner:
1308
1428
  exc_info=True,
1309
1429
  )
1310
1430
 
1311
- async def _propose_crashed_state(self, flow_run: "FlowRun", message: str) -> None:
1431
+ async def _propose_crashed_state(
1432
+ self, flow_run: "FlowRun", message: str
1433
+ ) -> State[Any] | None:
1312
1434
  run_logger = self._get_flow_run_logger(flow_run)
1435
+ state = None
1313
1436
  try:
1314
1437
  state = await propose_state(
1315
1438
  self._client,
@@ -1326,6 +1449,7 @@ class Runner:
1326
1449
  run_logger.info(
1327
1450
  f"Reported flow run '{flow_run.id}' as crashed: {message}"
1328
1451
  )
1452
+ return state
1329
1453
 
1330
1454
  async def _mark_flow_run_as_cancelled(
1331
1455
  self, flow_run: "FlowRun", state_updates: Optional[dict[str, Any]] = None
@@ -1391,9 +1515,14 @@ class Runner:
1391
1515
  """
1392
1516
  if state.is_cancelling():
1393
1517
  try:
1394
- flow = await load_flow_from_flow_run(
1395
- flow_run, storage_base_path=str(self._tmp_dir)
1396
- )
1518
+ if flow_run.id in self._flow_run_bundle_map:
1519
+ flow = extract_flow_from_bundle(
1520
+ self._flow_run_bundle_map[flow_run.id]
1521
+ )
1522
+ else:
1523
+ flow = await load_flow_from_flow_run(
1524
+ flow_run, storage_base_path=str(self._tmp_dir)
1525
+ )
1397
1526
  hooks = flow.on_cancellation_hooks or []
1398
1527
 
1399
1528
  await _run_hooks(hooks, flow_run, flow, state)
@@ -1412,9 +1541,12 @@ class Runner:
1412
1541
  Run the hooks for a flow.
1413
1542
  """
1414
1543
  if state.is_crashed():
1415
- flow = await load_flow_from_flow_run(
1416
- flow_run, storage_base_path=str(self._tmp_dir)
1417
- )
1544
+ if flow_run.id in self._flow_run_bundle_map:
1545
+ flow = extract_flow_from_bundle(self._flow_run_bundle_map[flow_run.id])
1546
+ else:
1547
+ flow = await load_flow_from_flow_run(
1548
+ flow_run, storage_base_path=str(self._tmp_dir)
1549
+ )
1418
1550
  hooks = flow.on_crashed_hooks or []
1419
1551
 
1420
1552
  await _run_hooks(hooks, flow_run, flow, state)
@@ -55,6 +55,7 @@ from prefect.settings import (
55
55
  PREFECT_DEBUG_MODE,
56
56
  PREFECT_MEMO_STORE_PATH,
57
57
  PREFECT_MEMOIZE_BLOCK_AUTO_REGISTRATION,
58
+ PREFECT_SERVER_API_BASE_PATH,
58
59
  PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS,
59
60
  PREFECT_UI_SERVE_BASE,
60
61
  get_current_settings,
@@ -356,7 +357,10 @@ def create_api_app(
356
357
  header_token = request.headers.get("Authorization")
357
358
 
358
359
  # used for probes in k8s and such
359
- if request.url.path in ["/api/health", "/api/ready"]:
360
+ if (
361
+ request.url.path.endswith(("health", "ready"))
362
+ and request.method.upper() == "GET"
363
+ ):
360
364
  return await call_next(request)
361
365
  try:
362
366
  if header_token is None:
@@ -691,7 +695,10 @@ def create_app(
691
695
  name="static",
692
696
  )
693
697
  app.api_app = api_app
694
- app.mount("/api", app=api_app, name="api")
698
+ if PREFECT_SERVER_API_BASE_PATH:
699
+ app.mount(PREFECT_SERVER_API_BASE_PATH.value(), app=api_app, name="api")
700
+ else:
701
+ app.mount("/api", app=api_app, name="api")
695
702
  app.mount("/", app=ui_app, name="ui")
696
703
 
697
704
  def openapi():
prefect/settings/base.py CHANGED
@@ -92,6 +92,7 @@ class PrefectBaseSettings(BaseSettings):
92
92
  self,
93
93
  exclude_unset: bool = False,
94
94
  include_secrets: bool = True,
95
+ include_aliases: bool = False,
95
96
  ) -> Dict[str, str]:
96
97
  """Convert the settings object to a dictionary of environment variables."""
97
98
  env: Dict[str, Any] = self.model_dump(
@@ -105,12 +106,26 @@ class PrefectBaseSettings(BaseSettings):
105
106
  child_env = child_settings.to_environment_variables(
106
107
  exclude_unset=exclude_unset,
107
108
  include_secrets=include_secrets,
109
+ include_aliases=include_aliases,
108
110
  )
109
111
  env_variables.update(child_env)
110
112
  elif (value := env.get(key)) is not None:
111
- env_variables[f"{self.model_config.get('env_prefix')}{key.upper()}"] = (
112
- _to_environment_variable_value(value)
113
- )
113
+ validation_alias = self.model_fields[key].validation_alias
114
+ if include_aliases and validation_alias is not None:
115
+ if isinstance(validation_alias, AliasChoices):
116
+ for alias in validation_alias.choices:
117
+ if isinstance(alias, str):
118
+ env_variables[alias.upper()] = (
119
+ _to_environment_variable_value(value)
120
+ )
121
+ elif isinstance(validation_alias, str):
122
+ env_variables[validation_alias.upper()] = (
123
+ _to_environment_variable_value(value)
124
+ )
125
+ else:
126
+ env_variables[
127
+ f"{self.model_config.get('env_prefix')}{key.upper()}"
128
+ ] = _to_environment_variable_value(value)
114
129
  return env_variables
115
130
 
116
131
  @model_serializer(
@@ -381,7 +381,11 @@ def _warn_on_misconfigured_api_url(settings: "Settings"):
381
381
  warnings_list.append(warning)
382
382
 
383
383
  parsed_url = urlparse(api_url)
384
- if parsed_url.path and not parsed_url.path.startswith("/api"):
384
+ if (
385
+ parsed_url.path
386
+ and "api.prefect.cloud" in api_url
387
+ and not parsed_url.path.startswith("/api")
388
+ ):
385
389
  warnings_list.append(
386
390
  "`PREFECT_API_URL` should have `/api` after the base URL."
387
391
  )
@@ -18,7 +18,7 @@ class ServerAPISettings(PrefectBaseSettings):
18
18
 
19
19
  auth_string: Optional[SecretStr] = Field(
20
20
  default=None,
21
- description="A string to use for basic authentication with the API; typically in the form 'user:password' but can be any string.",
21
+ description="A string to use for basic authentication with the API in the form 'user:password'.",
22
22
  )
23
23
 
24
24
  host: str = Field(
@@ -31,6 +31,12 @@ class ServerAPISettings(PrefectBaseSettings):
31
31
  description="The API's port address (defaults to `4200`).",
32
32
  )
33
33
 
34
+ base_path: Optional[str] = Field(
35
+ default=None,
36
+ description="The base URL path to serve the API under.",
37
+ examples=["/v2/api"],
38
+ )
39
+
34
40
  default_limit: int = Field(
35
41
  default=200,
36
42
  description="The default limit applied to queries that can return multiple objects, such as `POST /flow_runs/filter`.",
@@ -30,6 +30,20 @@ class SQLAlchemyConnectArgsSettings(PrefectBaseSettings):
30
30
  description="Controls the application_name field for connections opened from the connection pool when using a PostgreSQL database with the Prefect backend.",
31
31
  )
32
32
 
33
+ statement_cache_size: Optional[int] = Field(
34
+ default=None,
35
+ description="Controls statement cache size for PostgreSQL connections. Setting this to 0 is required when using PgBouncer in transaction mode. Defaults to None.",
36
+ )
37
+
38
+ prepared_statement_cache_size: Optional[int] = Field(
39
+ default=None,
40
+ description=(
41
+ "Controls the size of the statement cache for PostgreSQL connections. "
42
+ "When set to 0, statement caching is disabled. Defaults to None to use "
43
+ "SQLAlchemy's default behavior."
44
+ ),
45
+ )
46
+
33
47
 
34
48
  class SQLAlchemySettings(PrefectBaseSettings):
35
49
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.2.5
3
+ Version: 3.2.7
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io
@@ -1,19 +1,19 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/__init__.py,sha256=iCdcC5ZmeewikCdnPEP6YBAjPNV5dvfxpYCTpw30Hkw,3685
3
3
  prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
4
- prefect/_build_info.py,sha256=1vJdZsjkUYeGYwN7FaPcsa6XC6UFI8VkJdUSN-inOt8,180
4
+ prefect/_build_info.py,sha256=v72RCwnunuXLYjqd62c-v91feDIddMeaSrKisnM3Nqc,180
5
5
  prefect/_result_records.py,sha256=S6QmsODkehGVSzbMm6ig022PYbI6gNKz671p_8kBYx4,7789
6
6
  prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
7
7
  prefect/artifacts.py,sha256=dMBUOAWnUamzjb5HSqwB5-GR2Qb-Gxee26XG5NDCUuw,22720
8
8
  prefect/automations.py,sha256=ZzPxn2tINdlXTQo805V4rIlbXuNWxd7cdb3gTJxZIeY,12567
9
9
  prefect/cache_policies.py,sha256=cF_6eqg34x7XgaCIw6S8Vr-Eq0wIr4Y6t3FOuXaPBrY,11912
10
10
  prefect/context.py,sha256=iJe4pkFqX6lz8ax1Mde_YqVmBVWmzeBe0ca2_nT6KPQ,23673
11
- prefect/engine.py,sha256=4ZGTKFZA_t7K0XUSJqbJ6Ec20SFVFHasBTM--47fTyA,2610
11
+ prefect/engine.py,sha256=uB5JN4l045i5JTlRQNT1x7MwlSiGQ5Bop2Q6jHHOgxY,3699
12
12
  prefect/exceptions.py,sha256=-nih8qqdxRm6CX-4yrqwePVh8Mcpvla_V6N_KbdJsIU,11593
13
13
  prefect/filesystems.py,sha256=v5YqGB4uXf9Ew2VuB9VCSkawvYMMVvEtZf7w1VmAmr8,18036
14
- prefect/flow_engine.py,sha256=mW95w_fBpEPejYFXuMyjfnhm7J1jMSv_VtAYGD0VlCo,60226
14
+ prefect/flow_engine.py,sha256=gR44YU7aCAbHEqoMDdxL1SDrtS5Xx1Kzg3M7FWjHcvY,58967
15
15
  prefect/flow_runs.py,sha256=MzjfRFgQwOqUSC3Iuu6E0hWkWdn089Urk6BY3qjEwEE,16113
16
- prefect/flows.py,sha256=cp9TF3pSg73jhkL3SkzaUGbdU9hbsieKz95Wgfk-VA4,108408
16
+ prefect/flows.py,sha256=fE94Xxe2BsVwXoNLkrf57gN4rF-EfK5j1HQrwtXJMKo,108574
17
17
  prefect/futures.py,sha256=NYWGeC8uRGe1WWB1MxkUshdvAdYibhc32HdFjffdiW0,17217
18
18
  prefect/main.py,sha256=hFeTTrr01qWKcRwZVEHVipyHEybS0VLTscFV6zG6GtY,2306
19
19
  prefect/plugins.py,sha256=FPRLR2mWVBMuOnlzeiTD9krlHONZH2rtYLD753JQDNQ,2516
@@ -30,6 +30,7 @@ prefect/tasks.py,sha256=g__kIAXvbvdY7NtK7R-KbJg7qUP31xjf5uK80zRS3Ds,74049
30
30
  prefect/transactions.py,sha256=kOXwghBW3jM71gg49MkjJPTnImEzXWeTCUE_zpq2MlI,16068
31
31
  prefect/variables.py,sha256=dCK3vX7TbkqXZhnNT_v7rcGh3ISRqoR6pJVLpoll3Js,8342
32
32
  prefect/_experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ prefect/_experimental/bundles.py,sha256=7oumoEW7L6ZnRQJhO6ii3BsXIfPkE9Ic3yzX56vauR4,3821
33
34
  prefect/_experimental/lineage.py,sha256=8LssReoq7eLtQScUCu-7FCtrWoRZstXKRdpO0PxgbKg,9958
34
35
  prefect/_experimental/sla/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
36
  prefect/_experimental/sla/client.py,sha256=XTkYHFZiBy_O7RgUyGEdl9MxaHP-6fEAKBk3ksNQobU,3611
@@ -82,7 +83,7 @@ prefect/client/collections.py,sha256=t9XkVU_onQMZ871L21F1oZnAiPSQeeVfd_MuDEBS3iM
82
83
  prefect/client/constants.py,sha256=Z_GG8KF70vbbXxpJuqW5pLnwzujTVeHbcYYRikNmGH0,29
83
84
  prefect/client/subscriptions.py,sha256=TZ7Omv8yeQQIkE6EmWYM78e8p7UdvdTDzcQe91dCU4U,3838
84
85
  prefect/client/utilities.py,sha256=UEJD6nwYg2mD8-GSmru-E2ofXaBlmSFZ2-8T_5rIK6c,3472
85
- prefect/client/orchestration/__init__.py,sha256=uKWE1XNiwrOswgZ1JptffksRlji9PDrLcy8T9ZfNw1g,59596
86
+ prefect/client/orchestration/__init__.py,sha256=tP1zxHgrZaICchYM3mcK4zTUpCCihd-0v5V0lTmGivY,60699
86
87
  prefect/client/orchestration/base.py,sha256=HM6ryHBZSzuHoCFQM9u5qR5k1dN9Bbr_ah6z1UPNbZQ,1542
87
88
  prefect/client/orchestration/routes.py,sha256=JFG1OWUBfrxPKW8Q7XWItlhOrSZ67IOySSoFZ6mxzm0,4364
88
89
  prefect/client/orchestration/_artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -154,7 +155,7 @@ prefect/events/worker.py,sha256=HjbibR0_J1W1nnNMZDFTXAbB0cl_cFGaFI87DvNGcnI,4557
154
155
  prefect/events/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
156
  prefect/events/cli/automations.py,sha256=uCX3NnypoI25TmyAoyL6qYhanWjZbJ2watwv1nfQMxs,11513
156
157
  prefect/events/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
- prefect/events/schemas/automations.py,sha256=KE6tIQul--KdJhixMYSCBCpoPIEGPgkGraeLAl3kCS0,14538
158
+ prefect/events/schemas/automations.py,sha256=5uYx18sVf8Mqx-KtfcSGli8x4GkNPUHC8LZZfsDzeBo,14568
158
159
  prefect/events/schemas/deployment_triggers.py,sha256=OX9g9eHe0nqJ3PtVEzqs9Ub2LaOHMA4afLZSvSukKGU,3191
159
160
  prefect/events/schemas/events.py,sha256=CLh8nHl8zfy0RKvjVzK3qBg_Oe8opu2qyXpMYXqCnHM,9176
160
161
  prefect/events/schemas/labelling.py,sha256=bU-XYaHXhI2MEBIHngth96R9D02m8HHb85KNcHZ_1Gc,3073
@@ -174,7 +175,7 @@ prefect/locking/filesystem.py,sha256=O67Miiz466fQUu3UmHer9dkWpVL1f8GEX8Lv2lDj0Y8
174
175
  prefect/locking/memory.py,sha256=mFUgV750ywEL7aVQuxFjg9gxbjVU4esBQn7bGQYzeMY,7548
175
176
  prefect/locking/protocol.py,sha256=RsfvlaHTTEJ0YvYWSqFGoZuT2w4FPPxyQlHqjoyNGuE,4240
176
177
  prefect/logging/__init__.py,sha256=zx9f5_dWrR4DbcTOFBpNGOPoCZ1QcPFudr7zxb2XRpA,148
177
- prefect/logging/configuration.py,sha256=QIvmktuAZPteVnh8nd9jUb7vwGGkcUbBLyiti6XmbYM,3242
178
+ prefect/logging/configuration.py,sha256=XyHlC31XT00v3hsDg1KKKmKweb8E5fszVKE7BRQ-ebM,3292
178
179
  prefect/logging/filters.py,sha256=NnRYubh9dMmWcCAjuW32cIVQ37rLxdn8ci26wTtQMyU,1136
179
180
  prefect/logging/formatters.py,sha256=BkPykVyOFKdnhDj_1vhhOoWiHiiBeRnWXPcaRIWK3aI,4125
180
181
  prefect/logging/handlers.py,sha256=pIeS6gvuVnuh3lZ-kIC4ijRMSbVPkHo-rYeLMj5P8NA,12240
@@ -182,7 +183,7 @@ prefect/logging/highlighters.py,sha256=BCf_LNhFInIfGPqwuu8YVrGa4wVxNc4YXo2pYgftp
182
183
  prefect/logging/loggers.py,sha256=xkmHXsiuoPZZXcrrEgMA-ZQu0E-gW3tNVd4BIxWjnpM,12704
183
184
  prefect/logging/logging.yml,sha256=tT7gTyC4NmngFSqFkCdHaw7R0GPNPDDsTCGZQByiJAQ,3169
184
185
  prefect/runner/__init__.py,sha256=7U-vAOXFkzMfRz1q8Uv6Otsvc0OrPYLLP44srwkJ_8s,89
185
- prefect/runner/runner.py,sha256=bR3lR2hEKSHtd4V0gbEM7WDz6rx_trmWuovXy6Ifomc,55656
186
+ prefect/runner/runner.py,sha256=s40phNASP-dj-sDStQTucwaum3Khn4BmxJQzrnZXh1I,60966
186
187
  prefect/runner/server.py,sha256=WDDjCbnd2F_3LZBpVX2Y398xpmHvxjyBLKVHWkh5QxI,11240
187
188
  prefect/runner/storage.py,sha256=Uxx_7SPm-F0LR1LUq64cT-xHL2ofd37hHqLHtRYjGW0,27527
188
189
  prefect/runner/submit.py,sha256=3Ey6H4XrhYhCII4AobpvzZf21vAunWlMu40zAjMC0gc,8353
@@ -216,7 +217,7 @@ prefect/server/api/middleware.py,sha256=WkyuyeJIfo9Q0GAIVU5gO6yIGNVwoHwuBah5AB5o
216
217
  prefect/server/api/root.py,sha256=CeumFYIM_BDvPicJH9ry5PO_02PZTLeMqbLMGGTh90o,942
217
218
  prefect/server/api/run_history.py,sha256=FHepAgo1AYFeuh7rrAVzo_o3hu8Uc8-4DeH5aD5VGgw,5995
218
219
  prefect/server/api/saved_searches.py,sha256=UjoqLLe245QVIs6q5Vk4vdODCOoYzciEEjhi7B8sYCE,3233
219
- prefect/server/api/server.py,sha256=mAAZ_l0tfcpdnNDvP7-uP8w3f5HkjitpKZlyFBhLrYA,31835
220
+ prefect/server/api/server.py,sha256=W85DhOyIyq5_Cf5ep-YrNd8vF2IB-JzKzqMppIox9Fs,32082
220
221
  prefect/server/api/task_run_states.py,sha256=qlMUR4cH943EVgAkHHwTyHznb613VNa4WErEHdwcU60,1568
221
222
  prefect/server/api/task_runs.py,sha256=_rZXe2ZJDnOr3kf2MfBPMUI3N_6lbcVkJxvl-A1IKDQ,12160
222
223
  prefect/server/api/task_workers.py,sha256=TLvw0DckIqAeIaS3jky9zEF3nT2FII2F7oEE5Kf_13U,950
@@ -233,7 +234,7 @@ prefect/server/api/ui/flows.py,sha256=-g1xKHGBwh59CiILvrK3qlSE9g4boupCh4fl8UvF-i
233
234
  prefect/server/api/ui/schemas.py,sha256=moKeBqG9qdCo6yHoxeDnmvHrGoLCerHc4GMEQwzqleU,2050
234
235
  prefect/server/api/ui/task_runs.py,sha256=9zKN96k9GD5uUzKWNq_l-JA8ui4OqRhjyN7B3V8IHXs,6667
235
236
  prefect/settings/__init__.py,sha256=3jDLzExmq9HsRWo1kTSE16BO_3B3JlVsk5pR0s4PWEQ,2136
236
- prefect/settings/base.py,sha256=IWCFoDLKecoSlEtscwVlBwbC6KgzBHHwYODhLlOdWX8,8821
237
+ prefect/settings/base.py,sha256=e5huXB_cWgw_N6QWuRNSWMjW6B-g261RbiQ-6RoWFVE,9667
237
238
  prefect/settings/constants.py,sha256=5NjVLG1Km9J9I-a6wrq-qmi_dTkPdwEk3IrY9bSxWvw,281
238
239
  prefect/settings/context.py,sha256=yKxnaDJHX8e2jmAVtw1RF9o7X4V3AOcz61sVeQyPX2c,2195
239
240
  prefect/settings/legacy.py,sha256=KG00GwaURl1zbwfCKAjwNRdJjB2UdTyo80gYF7U60jk,5693
@@ -251,14 +252,14 @@ prefect/settings/models/flows.py,sha256=kQ_sCA7TUqaEs9wWuGHkGQOuAIEZ5elD4UzeKRe0
251
252
  prefect/settings/models/internal.py,sha256=KUb16dg3lH5gwlnUnVJub6JHFXHRyZf1voINBvC_Ysc,718
252
253
  prefect/settings/models/logging.py,sha256=Sj9GDNr5QMFaP6CN0WJyfpwhpOk4p1yhv45dyQMRzHM,4729
253
254
  prefect/settings/models/results.py,sha256=VWFplQSSJyc0LXnziw1H5b3N_PDS32QBe_q2MWwYni0,1484
254
- prefect/settings/models/root.py,sha256=iExDLqNnaxNYhdWdRWNHUsMX9O_fMXczrGsNInEpmYY,16466
255
+ prefect/settings/models/root.py,sha256=b9ZamM-BcD75Xsk0pZej3o2ix2kHkTP14hhITveFcTo,16549
255
256
  prefect/settings/models/runner.py,sha256=rD8OmNLwILmqnGe9YkM1dWKsulx3clYm4LI5N9vD5yM,1991
256
257
  prefect/settings/models/tasks.py,sha256=3XSMhOsBFoSOL3e-CL36uOuL-zsl9c3buvTNyGubLWg,3482
257
258
  prefect/settings/models/testing.py,sha256=j9YH_WkB14iEzOjUtTmvY978qRSbgCypFSEi_cOs8no,1820
258
259
  prefect/settings/models/worker.py,sha256=zeDU71aR4CEvEOKyH-1jgEyol8XYe29PExjIC6a8Wv0,1378
259
260
  prefect/settings/models/server/__init__.py,sha256=KJmffmlHb8GYnClaeYcerae-IaeNsNMucKKRRS_zG9Q,33
260
- prefect/settings/models/server/api.py,sha256=jNmPqEg96h6Q8VFVZfLcAFLWRzzTqtCm2n8CvoA2QxY,5073
261
- prefect/settings/models/server/database.py,sha256=dJplrENm6f7Wo1XJ7GVKo5hK58vZrEfYXSJ19NCEUfc,10573
261
+ prefect/settings/models/server/api.py,sha256=fhj9pt6RGtUHkyriaTPto4NnOwJD4XWLdIyxuiJ2Dzk,5202
262
+ prefect/settings/models/server/database.py,sha256=Ilw452gS4L1L4tk53JP-G080JblPVdWzdSVxhfuXcXQ,11156
262
263
  prefect/settings/models/server/deployments.py,sha256=LjWQr2U1mjItYhuuLqMT_QQ7P4KHqC-tKFfA-rEKefs,898
263
264
  prefect/settings/models/server/ephemeral.py,sha256=rh8Py5Nxh-gq9KgfB7CDnIgT_nuOuv59OrLGuhMIGmk,1043
264
265
  prefect/settings/models/server/events.py,sha256=s766htbPv2yx7w-09i8BrtAZ_js--AtHYtvXeK7puIk,5578
@@ -316,7 +317,7 @@ prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
316
317
  prefect/workers/process.py,sha256=6VWon_LK7fQNLlQTjTBFeU4KFUa4faqP4EUuTvrbtbg,20176
317
318
  prefect/workers/server.py,sha256=SEuyScZ5nGm2OotdtbHjpvqJlTRVWCh29ND7FeL_fZA,1974
318
319
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
319
- prefect_client-3.2.5.dist-info/METADATA,sha256=OBGigaKyErFGzBn7ZR8OnRnXfrMqrgK8gmbi24WZkxI,7192
320
- prefect_client-3.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
321
- prefect_client-3.2.5.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
322
- prefect_client-3.2.5.dist-info/RECORD,,
320
+ prefect_client-3.2.7.dist-info/METADATA,sha256=ivVP_miQGh7GYKttza4VAFCOVhdJEK2vd2UQDXE-Kco,7192
321
+ prefect_client-3.2.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
322
+ prefect_client-3.2.7.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
323
+ prefect_client-3.2.7.dist-info/RECORD,,