prefect-client 3.1.9__py3-none-any.whl → 3.1.11__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 (113) hide show
  1. prefect/_experimental/lineage.py +7 -8
  2. prefect/_internal/_logging.py +15 -3
  3. prefect/_internal/compatibility/async_dispatch.py +22 -16
  4. prefect/_internal/compatibility/deprecated.py +42 -18
  5. prefect/_internal/compatibility/migration.py +2 -2
  6. prefect/_internal/concurrency/inspection.py +12 -14
  7. prefect/_internal/concurrency/primitives.py +2 -2
  8. prefect/_internal/concurrency/services.py +154 -80
  9. prefect/_internal/concurrency/waiters.py +13 -9
  10. prefect/_internal/pydantic/annotations/pendulum.py +7 -7
  11. prefect/_internal/pytz.py +4 -3
  12. prefect/_internal/retries.py +10 -5
  13. prefect/_internal/schemas/bases.py +19 -10
  14. prefect/_internal/schemas/validators.py +227 -388
  15. prefect/_version.py +3 -3
  16. prefect/artifacts.py +61 -74
  17. prefect/automations.py +27 -7
  18. prefect/blocks/core.py +3 -3
  19. prefect/client/{orchestration.py → orchestration/__init__.py} +38 -701
  20. prefect/client/orchestration/_artifacts/__init__.py +0 -0
  21. prefect/client/orchestration/_artifacts/client.py +239 -0
  22. prefect/client/orchestration/_concurrency_limits/__init__.py +0 -0
  23. prefect/client/orchestration/_concurrency_limits/client.py +762 -0
  24. prefect/client/orchestration/_logs/__init__.py +0 -0
  25. prefect/client/orchestration/_logs/client.py +95 -0
  26. prefect/client/orchestration/_variables/__init__.py +0 -0
  27. prefect/client/orchestration/_variables/client.py +157 -0
  28. prefect/client/orchestration/base.py +46 -0
  29. prefect/client/orchestration/routes.py +145 -0
  30. prefect/client/schemas/actions.py +2 -2
  31. prefect/client/schemas/filters.py +5 -0
  32. prefect/client/schemas/objects.py +3 -10
  33. prefect/client/schemas/schedules.py +22 -10
  34. prefect/concurrency/_asyncio.py +87 -0
  35. prefect/concurrency/{events.py → _events.py} +10 -10
  36. prefect/concurrency/asyncio.py +20 -104
  37. prefect/concurrency/context.py +6 -4
  38. prefect/concurrency/services.py +26 -74
  39. prefect/concurrency/sync.py +23 -44
  40. prefect/concurrency/v1/_asyncio.py +63 -0
  41. prefect/concurrency/v1/{events.py → _events.py} +13 -15
  42. prefect/concurrency/v1/asyncio.py +27 -80
  43. prefect/concurrency/v1/context.py +6 -4
  44. prefect/concurrency/v1/services.py +33 -79
  45. prefect/concurrency/v1/sync.py +18 -37
  46. prefect/context.py +66 -70
  47. prefect/deployments/base.py +4 -144
  48. prefect/deployments/flow_runs.py +12 -2
  49. prefect/deployments/runner.py +11 -3
  50. prefect/deployments/steps/pull.py +13 -0
  51. prefect/events/clients.py +7 -1
  52. prefect/events/schemas/events.py +3 -2
  53. prefect/flow_engine.py +54 -47
  54. prefect/flows.py +2 -1
  55. prefect/futures.py +42 -27
  56. prefect/input/run_input.py +2 -1
  57. prefect/locking/filesystem.py +8 -7
  58. prefect/locking/memory.py +5 -3
  59. prefect/locking/protocol.py +1 -1
  60. prefect/main.py +1 -3
  61. prefect/plugins.py +12 -10
  62. prefect/results.py +3 -308
  63. prefect/runner/storage.py +87 -21
  64. prefect/serializers.py +32 -25
  65. prefect/settings/legacy.py +4 -4
  66. prefect/settings/models/api.py +3 -3
  67. prefect/settings/models/cli.py +3 -3
  68. prefect/settings/models/client.py +5 -3
  69. prefect/settings/models/cloud.py +3 -3
  70. prefect/settings/models/deployments.py +3 -3
  71. prefect/settings/models/experiments.py +4 -2
  72. prefect/settings/models/flows.py +3 -3
  73. prefect/settings/models/internal.py +4 -2
  74. prefect/settings/models/logging.py +4 -3
  75. prefect/settings/models/results.py +3 -3
  76. prefect/settings/models/root.py +3 -2
  77. prefect/settings/models/runner.py +4 -4
  78. prefect/settings/models/server/api.py +3 -3
  79. prefect/settings/models/server/database.py +11 -4
  80. prefect/settings/models/server/deployments.py +6 -2
  81. prefect/settings/models/server/ephemeral.py +4 -2
  82. prefect/settings/models/server/events.py +3 -2
  83. prefect/settings/models/server/flow_run_graph.py +6 -2
  84. prefect/settings/models/server/root.py +3 -3
  85. prefect/settings/models/server/services.py +26 -11
  86. prefect/settings/models/server/tasks.py +6 -3
  87. prefect/settings/models/server/ui.py +3 -3
  88. prefect/settings/models/tasks.py +5 -5
  89. prefect/settings/models/testing.py +3 -3
  90. prefect/settings/models/worker.py +5 -3
  91. prefect/settings/profiles.py +15 -2
  92. prefect/states.py +4 -7
  93. prefect/task_engine.py +54 -75
  94. prefect/tasks.py +84 -32
  95. prefect/telemetry/processors.py +6 -6
  96. prefect/telemetry/run_telemetry.py +13 -8
  97. prefect/telemetry/services.py +32 -31
  98. prefect/transactions.py +4 -15
  99. prefect/utilities/_git.py +34 -0
  100. prefect/utilities/asyncutils.py +1 -1
  101. prefect/utilities/engine.py +3 -19
  102. prefect/utilities/generics.py +18 -0
  103. prefect/workers/__init__.py +2 -0
  104. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/METADATA +1 -1
  105. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/RECORD +108 -99
  106. prefect/records/__init__.py +0 -1
  107. prefect/records/base.py +0 -235
  108. prefect/records/filesystem.py +0 -213
  109. prefect/records/memory.py +0 -184
  110. prefect/records/result_store.py +0 -70
  111. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/LICENSE +0 -0
  112. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/WHEEL +0 -0
  113. {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,30 @@
1
- import asyncio
1
+ from collections.abc import AsyncGenerator
2
2
  from contextlib import asynccontextmanager
3
- from typing import AsyncGenerator, List, Optional, Union, cast
3
+ from typing import TYPE_CHECKING, Optional, Union
4
4
  from uuid import UUID
5
5
 
6
6
  import anyio
7
- import httpx
8
7
  import pendulum
9
8
 
10
- from ...client.schemas.responses import MinimalConcurrencyLimitResponse
11
-
12
- try:
13
- from pendulum import Interval
14
- except ImportError:
15
- # pendulum < 3
16
- from pendulum.period import Period as Interval # type: ignore
17
-
18
- from prefect.client.orchestration import get_client
19
- from prefect.utilities.asyncutils import sync_compatible
20
-
21
- from .context import ConcurrencyContext
22
- from .events import (
23
- _emit_concurrency_acquisition_events,
24
- _emit_concurrency_release_events,
9
+ from prefect.concurrency.v1._asyncio import (
10
+ acquire_concurrency_slots,
11
+ release_concurrency_slots,
25
12
  )
26
- from .services import ConcurrencySlotAcquisitionService
27
-
28
-
29
- class ConcurrencySlotAcquisitionError(Exception):
30
- """Raised when an unhandlable occurs while acquiring concurrency slots."""
31
-
13
+ from prefect.concurrency.v1._events import (
14
+ emit_concurrency_acquisition_events,
15
+ emit_concurrency_release_events,
16
+ )
17
+ from prefect.concurrency.v1.context import ConcurrencyContext
32
18
 
33
- class AcquireConcurrencySlotTimeoutError(TimeoutError):
34
- """Raised when acquiring a concurrency slot times out."""
19
+ from ._asyncio import (
20
+ AcquireConcurrencySlotTimeoutError as AcquireConcurrencySlotTimeoutError,
21
+ )
22
+ from ._asyncio import ConcurrencySlotAcquisitionError as ConcurrencySlotAcquisitionError
35
23
 
36
24
 
37
25
  @asynccontextmanager
38
26
  async def concurrency(
39
- names: Union[str, List[str]],
27
+ names: Union[str, list[str]],
40
28
  task_run_id: UUID,
41
29
  timeout_seconds: Optional[float] = None,
42
30
  ) -> AsyncGenerator[None, None]:
@@ -69,24 +57,30 @@ async def concurrency(
69
57
  yield
70
58
  return
71
59
 
72
- names_normalized: List[str] = names if isinstance(names, list) else [names]
60
+ names_normalized: list[str] = names if isinstance(names, list) else [names]
73
61
 
74
- limits = await _acquire_concurrency_slots(
62
+ acquire_slots = acquire_concurrency_slots(
75
63
  names_normalized,
76
64
  task_run_id=task_run_id,
77
65
  timeout_seconds=timeout_seconds,
78
66
  )
67
+ if TYPE_CHECKING:
68
+ assert not isinstance(acquire_slots, list)
69
+ limits = await acquire_slots
79
70
  acquisition_time = pendulum.now("UTC")
80
- emitted_events = _emit_concurrency_acquisition_events(limits, task_run_id)
71
+ emitted_events = emit_concurrency_acquisition_events(limits, task_run_id)
81
72
 
82
73
  try:
83
74
  yield
84
75
  finally:
85
- occupancy_period = cast(Interval, (pendulum.now("UTC") - acquisition_time))
76
+ occupancy_period = pendulum.now("UTC") - acquisition_time
86
77
  try:
87
- await _release_concurrency_slots(
78
+ release_slots = release_concurrency_slots(
88
79
  names_normalized, task_run_id, occupancy_period.total_seconds()
89
80
  )
81
+ if TYPE_CHECKING:
82
+ assert not isinstance(release_slots, list)
83
+ await release_slots
90
84
  except anyio.get_cancelled_exc_class():
91
85
  # The task was cancelled before it could release the slots. Add the
92
86
  # slots to the cleanup list so they can be released when the
@@ -96,51 +90,4 @@ async def concurrency(
96
90
  (names_normalized, occupancy_period.total_seconds(), task_run_id)
97
91
  )
98
92
 
99
- _emit_concurrency_release_events(limits, emitted_events, task_run_id)
100
-
101
-
102
- @sync_compatible
103
- async def _acquire_concurrency_slots(
104
- names: List[str],
105
- task_run_id: UUID,
106
- timeout_seconds: Optional[float] = None,
107
- ) -> List[MinimalConcurrencyLimitResponse]:
108
- service = ConcurrencySlotAcquisitionService.instance(frozenset(names))
109
- future = service.send((task_run_id, timeout_seconds))
110
- response_or_exception = await asyncio.wrap_future(future)
111
-
112
- if isinstance(response_or_exception, Exception):
113
- if isinstance(response_or_exception, TimeoutError):
114
- raise AcquireConcurrencySlotTimeoutError(
115
- f"Attempt to acquire concurrency limits timed out after {timeout_seconds} second(s)"
116
- ) from response_or_exception
117
-
118
- raise ConcurrencySlotAcquisitionError(
119
- f"Unable to acquire concurrency limits {names!r}"
120
- ) from response_or_exception
121
-
122
- return _response_to_concurrency_limit_response(response_or_exception)
123
-
124
-
125
- @sync_compatible
126
- async def _release_concurrency_slots(
127
- names: List[str],
128
- task_run_id: UUID,
129
- occupancy_seconds: float,
130
- ) -> List[MinimalConcurrencyLimitResponse]:
131
- async with get_client() as client:
132
- response = await client.decrement_v1_concurrency_slots(
133
- names=names,
134
- task_run_id=task_run_id,
135
- occupancy_seconds=occupancy_seconds,
136
- )
137
- return _response_to_concurrency_limit_response(response)
138
-
139
-
140
- def _response_to_concurrency_limit_response(
141
- response: httpx.Response,
142
- ) -> List[MinimalConcurrencyLimitResponse]:
143
- data = response.json() or []
144
- return [
145
- MinimalConcurrencyLimitResponse.model_validate(limit) for limit in data if data
146
- ]
93
+ emit_concurrency_release_events(limits, emitted_events, task_run_id)
@@ -1,20 +1,22 @@
1
1
  from contextvars import ContextVar
2
- from typing import List, Tuple
2
+ from typing import Any, ClassVar
3
3
  from uuid import UUID
4
4
 
5
+ from typing_extensions import Self
6
+
5
7
  from prefect.client.orchestration import get_client
6
8
  from prefect.context import ContextModel, Field
7
9
 
8
10
 
9
11
  class ConcurrencyContext(ContextModel):
10
- __var__: ContextVar = ContextVar("concurrency_v1")
12
+ __var__: ClassVar[ContextVar[Self]] = ContextVar("concurrency_v1")
11
13
 
12
14
  # Track the limits that have been acquired but were not able to be released
13
15
  # due to cancellation or some other error. These limits are released when
14
16
  # the context manager exits.
15
- cleanup_slots: List[Tuple[List[str], float, UUID]] = Field(default_factory=list)
17
+ cleanup_slots: list[tuple[list[str], float, UUID]] = Field(default_factory=list)
16
18
 
17
- def __exit__(self, *exc_info):
19
+ def __exit__(self, *exc_info: Any) -> None:
18
20
  if self.cleanup_slots:
19
21
  with get_client(sync_client=True) as client:
20
22
  for names, occupancy_seconds, task_run_id in self.cleanup_slots:
@@ -1,21 +1,16 @@
1
1
  import asyncio
2
- import concurrent.futures
2
+ from collections.abc import AsyncGenerator
3
3
  from contextlib import asynccontextmanager
4
4
  from json import JSONDecodeError
5
- from typing import (
6
- TYPE_CHECKING,
7
- AsyncGenerator,
8
- FrozenSet,
9
- Optional,
10
- Tuple,
11
- )
5
+ from typing import TYPE_CHECKING, Optional
12
6
  from uuid import UUID
13
7
 
14
8
  import httpx
15
9
  from starlette import status
10
+ from typing_extensions import Unpack
16
11
 
17
12
  from prefect._internal.concurrency import logger
18
- from prefect._internal.concurrency.services import QueueService
13
+ from prefect._internal.concurrency.services import FutureQueueService
19
14
  from prefect.client.orchestration import get_client
20
15
  from prefect.utilities.timeout import timeout_async
21
16
 
@@ -27,11 +22,13 @@ class ConcurrencySlotAcquisitionServiceError(Exception):
27
22
  """Raised when an error occurs while acquiring concurrency slots."""
28
23
 
29
24
 
30
- class ConcurrencySlotAcquisitionService(QueueService):
31
- def __init__(self, concurrency_limit_names: FrozenSet[str]):
25
+ class ConcurrencySlotAcquisitionService(
26
+ FutureQueueService[Unpack[tuple[UUID, Optional[float]]], httpx.Response]
27
+ ):
28
+ def __init__(self, concurrency_limit_names: frozenset[str]) -> None:
32
29
  super().__init__(concurrency_limit_names)
33
- self._client: "PrefectClient"
34
- self.concurrency_limit_names = sorted(list(concurrency_limit_names))
30
+ self._client: PrefectClient
31
+ self.concurrency_limit_names: list[str] = sorted(list(concurrency_limit_names))
35
32
 
36
33
  @asynccontextmanager
37
34
  async def _lifespan(self) -> AsyncGenerator[None, None]:
@@ -39,78 +36,35 @@ class ConcurrencySlotAcquisitionService(QueueService):
39
36
  self._client = client
40
37
  yield
41
38
 
42
- async def _handle(
43
- self,
44
- item: Tuple[
45
- UUID,
46
- concurrent.futures.Future,
47
- Optional[float],
48
- ],
49
- ) -> None:
50
- task_run_id, future, timeout_seconds = item
51
- try:
52
- response = await self.acquire_slots(task_run_id, timeout_seconds)
53
- except Exception as exc:
54
- # If the request to the increment endpoint fails in a non-standard
55
- # way, we need to set the future's result so that the caller can
56
- # handle the exception and then re-raise.
57
- future.set_result(exc)
58
- raise exc
59
- else:
60
- future.set_result(response)
61
-
62
- async def acquire_slots(
63
- self,
64
- task_run_id: UUID,
65
- timeout_seconds: Optional[float] = None,
39
+ async def acquire(
40
+ self, task_run_id: UUID, timeout_seconds: Optional[float] = None
66
41
  ) -> httpx.Response:
67
42
  with timeout_async(seconds=timeout_seconds):
68
43
  while True:
69
44
  try:
70
- response = await self._client.increment_v1_concurrency_slots(
45
+ return await self._client.increment_v1_concurrency_slots(
71
46
  task_run_id=task_run_id,
72
47
  names=self.concurrency_limit_names,
73
48
  )
74
- except Exception as exc:
75
- if (
76
- isinstance(exc, httpx.HTTPStatusError)
77
- and exc.response.status_code == status.HTTP_423_LOCKED
78
- ):
79
- retry_after = exc.response.headers.get("Retry-After")
80
- if retry_after:
81
- retry_after = float(retry_after)
82
- await asyncio.sleep(retry_after)
83
- else:
84
- # We received a 423 but no Retry-After header. This
85
- # should indicate that the server told us to abort
86
- # because the concurrency limit is set to 0, i.e.
87
- # effectively disabled.
88
- try:
89
- reason = exc.response.json()["detail"]
90
- except (JSONDecodeError, KeyError):
91
- logger.error(
92
- "Failed to parse response from concurrency limit 423 Locked response: %s",
93
- exc.response.content,
94
- )
95
- reason = "Concurrency limit is locked (server did not specify the reason)"
96
- raise ConcurrencySlotAcquisitionServiceError(
97
- reason
98
- ) from exc
49
+ except httpx.HTTPStatusError as exc:
50
+ if not exc.response.status_code == status.HTTP_423_LOCKED:
51
+ raise
99
52
 
53
+ retry_after = exc.response.headers.get("Retry-After")
54
+ if retry_after:
55
+ retry_after = float(retry_after)
56
+ await asyncio.sleep(retry_after)
100
57
  else:
101
- raise exc # type: ignore
102
- else:
103
- return response
104
-
105
- def send(self, item: Tuple[UUID, Optional[float]]) -> concurrent.futures.Future:
106
- with self._lock:
107
- if self._stopped:
108
- raise RuntimeError("Cannot put items in a stopped service instance.")
109
-
110
- logger.debug("Service %r enqueuing item %r", self, item)
111
- future: concurrent.futures.Future = concurrent.futures.Future()
112
-
113
- task_run_id, timeout_seconds = item
114
- self._queue.put_nowait((task_run_id, future, timeout_seconds))
115
-
116
- return future
58
+ # We received a 423 but no Retry-After header. This
59
+ # should indicate that the server told us to abort
60
+ # because the concurrency limit is set to 0, i.e.
61
+ # effectively disabled.
62
+ try:
63
+ reason = exc.response.json()["detail"]
64
+ except (JSONDecodeError, KeyError):
65
+ logger.error(
66
+ "Failed to parse response from concurrency limit 423 Locked response: %s",
67
+ exc.response.content,
68
+ )
69
+ reason = "Concurrency limit is locked (server did not specify the reason)"
70
+ raise ConcurrencySlotAcquisitionServiceError(reason) from exc
@@ -1,31 +1,15 @@
1
+ import asyncio
2
+ from collections.abc import Generator
1
3
  from contextlib import contextmanager
2
- from typing import (
3
- Generator,
4
- List,
5
- Optional,
6
- TypeVar,
7
- Union,
8
- cast,
9
- )
4
+ from typing import Optional, TypeVar, Union
10
5
  from uuid import UUID
11
6
 
12
7
  import pendulum
13
8
 
14
- from ...client.schemas.responses import MinimalConcurrencyLimitResponse
15
-
16
- try:
17
- from pendulum import Interval
18
- except ImportError:
19
- # pendulum < 3
20
- from pendulum.period import Period as Interval # type: ignore
21
-
22
- from .asyncio import (
23
- _acquire_concurrency_slots,
24
- _release_concurrency_slots,
25
- )
26
- from .events import (
27
- _emit_concurrency_acquisition_events,
28
- _emit_concurrency_release_events,
9
+ from ._asyncio import acquire_concurrency_slots, release_concurrency_slots
10
+ from ._events import (
11
+ emit_concurrency_acquisition_events,
12
+ emit_concurrency_release_events,
29
13
  )
30
14
 
31
15
  T = TypeVar("T")
@@ -33,7 +17,7 @@ T = TypeVar("T")
33
17
 
34
18
  @contextmanager
35
19
  def concurrency(
36
- names: Union[str, List[str]],
20
+ names: Union[str, list[str]],
37
21
  task_run_id: UUID,
38
22
  timeout_seconds: Optional[float] = None,
39
23
  ) -> Generator[None, None, None]:
@@ -69,23 +53,20 @@ def concurrency(
69
53
 
70
54
  names = names if isinstance(names, list) else [names]
71
55
 
72
- limits: List[MinimalConcurrencyLimitResponse] = _acquire_concurrency_slots(
73
- names,
74
- timeout_seconds=timeout_seconds,
75
- task_run_id=task_run_id,
76
- _sync=True,
56
+ force = {"_sync": True}
57
+ result = acquire_concurrency_slots(
58
+ names, timeout_seconds=timeout_seconds, task_run_id=task_run_id, **force
77
59
  )
60
+ assert not asyncio.iscoroutine(result)
61
+ limits = result
78
62
  acquisition_time = pendulum.now("UTC")
79
- emitted_events = _emit_concurrency_acquisition_events(limits, task_run_id)
63
+ emitted_events = emit_concurrency_acquisition_events(limits, task_run_id)
80
64
 
81
65
  try:
82
66
  yield
83
67
  finally:
84
- occupancy_period = cast(Interval, pendulum.now("UTC") - acquisition_time)
85
- _release_concurrency_slots(
86
- names,
87
- task_run_id,
88
- occupancy_period.total_seconds(),
89
- _sync=True,
68
+ occupancy_period = pendulum.now("UTC") - acquisition_time
69
+ release_concurrency_slots(
70
+ names, task_run_id, occupancy_period.total_seconds(), **force
90
71
  )
91
- _emit_concurrency_release_events(limits, emitted_events, task_run_id)
72
+ emit_concurrency_release_events(limits, emitted_events, task_run_id)