prefect-client 3.1.5__py3-none-any.whl → 3.1.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.
Files changed (114) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_experimental/__init__.py +0 -0
  3. prefect/_experimental/lineage.py +181 -0
  4. prefect/_internal/compatibility/async_dispatch.py +38 -9
  5. prefect/_internal/compatibility/migration.py +1 -1
  6. prefect/_internal/concurrency/api.py +52 -52
  7. prefect/_internal/concurrency/calls.py +59 -35
  8. prefect/_internal/concurrency/cancellation.py +34 -18
  9. prefect/_internal/concurrency/event_loop.py +7 -6
  10. prefect/_internal/concurrency/threads.py +41 -33
  11. prefect/_internal/concurrency/waiters.py +28 -21
  12. prefect/_internal/pydantic/v1_schema.py +2 -2
  13. prefect/_internal/pydantic/v2_schema.py +10 -9
  14. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  15. prefect/_internal/retries.py +15 -6
  16. prefect/_internal/schemas/bases.py +11 -8
  17. prefect/_internal/schemas/validators.py +7 -5
  18. prefect/_version.py +3 -3
  19. prefect/automations.py +53 -47
  20. prefect/blocks/abstract.py +12 -10
  21. prefect/blocks/core.py +148 -19
  22. prefect/blocks/system.py +2 -1
  23. prefect/cache_policies.py +11 -11
  24. prefect/client/__init__.py +3 -1
  25. prefect/client/base.py +36 -37
  26. prefect/client/cloud.py +26 -19
  27. prefect/client/collections.py +2 -2
  28. prefect/client/orchestration.py +430 -273
  29. prefect/client/schemas/__init__.py +24 -0
  30. prefect/client/schemas/actions.py +128 -121
  31. prefect/client/schemas/filters.py +1 -1
  32. prefect/client/schemas/objects.py +114 -85
  33. prefect/client/schemas/responses.py +19 -20
  34. prefect/client/schemas/schedules.py +136 -93
  35. prefect/client/subscriptions.py +30 -15
  36. prefect/client/utilities.py +46 -36
  37. prefect/concurrency/asyncio.py +6 -9
  38. prefect/concurrency/sync.py +35 -5
  39. prefect/context.py +40 -32
  40. prefect/deployments/flow_runs.py +6 -8
  41. prefect/deployments/runner.py +14 -14
  42. prefect/deployments/steps/core.py +3 -1
  43. prefect/deployments/steps/pull.py +60 -12
  44. prefect/docker/__init__.py +1 -1
  45. prefect/events/clients.py +55 -4
  46. prefect/events/filters.py +1 -1
  47. prefect/events/related.py +2 -1
  48. prefect/events/schemas/events.py +26 -21
  49. prefect/events/utilities.py +3 -2
  50. prefect/events/worker.py +8 -0
  51. prefect/filesystems.py +3 -3
  52. prefect/flow_engine.py +87 -87
  53. prefect/flow_runs.py +7 -5
  54. prefect/flows.py +218 -176
  55. prefect/logging/configuration.py +1 -1
  56. prefect/logging/highlighters.py +1 -2
  57. prefect/logging/loggers.py +30 -20
  58. prefect/main.py +17 -24
  59. prefect/results.py +43 -22
  60. prefect/runner/runner.py +43 -21
  61. prefect/runner/server.py +30 -32
  62. prefect/runner/storage.py +3 -3
  63. prefect/runner/submit.py +3 -6
  64. prefect/runner/utils.py +6 -6
  65. prefect/runtime/flow_run.py +7 -0
  66. prefect/serializers.py +28 -24
  67. prefect/settings/constants.py +2 -2
  68. prefect/settings/legacy.py +1 -1
  69. prefect/settings/models/experiments.py +5 -0
  70. prefect/settings/models/server/events.py +10 -0
  71. prefect/task_engine.py +87 -26
  72. prefect/task_runners.py +2 -2
  73. prefect/task_worker.py +43 -25
  74. prefect/tasks.py +148 -142
  75. prefect/telemetry/bootstrap.py +15 -2
  76. prefect/telemetry/instrumentation.py +1 -1
  77. prefect/telemetry/processors.py +10 -7
  78. prefect/telemetry/run_telemetry.py +231 -0
  79. prefect/transactions.py +14 -14
  80. prefect/types/__init__.py +5 -5
  81. prefect/utilities/_engine.py +96 -0
  82. prefect/utilities/annotations.py +25 -18
  83. prefect/utilities/asyncutils.py +126 -140
  84. prefect/utilities/callables.py +87 -78
  85. prefect/utilities/collections.py +278 -117
  86. prefect/utilities/compat.py +13 -21
  87. prefect/utilities/context.py +6 -5
  88. prefect/utilities/dispatch.py +23 -12
  89. prefect/utilities/dockerutils.py +33 -32
  90. prefect/utilities/engine.py +126 -239
  91. prefect/utilities/filesystem.py +18 -15
  92. prefect/utilities/hashing.py +10 -11
  93. prefect/utilities/importtools.py +40 -27
  94. prefect/utilities/math.py +9 -5
  95. prefect/utilities/names.py +3 -3
  96. prefect/utilities/processutils.py +121 -57
  97. prefect/utilities/pydantic.py +41 -36
  98. prefect/utilities/render_swagger.py +22 -12
  99. prefect/utilities/schema_tools/__init__.py +2 -1
  100. prefect/utilities/schema_tools/hydration.py +50 -43
  101. prefect/utilities/schema_tools/validation.py +52 -42
  102. prefect/utilities/services.py +13 -12
  103. prefect/utilities/templating.py +45 -45
  104. prefect/utilities/text.py +2 -1
  105. prefect/utilities/timeout.py +4 -4
  106. prefect/utilities/urls.py +9 -4
  107. prefect/utilities/visualization.py +46 -24
  108. prefect/variables.py +136 -27
  109. prefect/workers/base.py +15 -8
  110. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
  111. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
  112. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
  113. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
  114. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,17 @@
1
1
  import time
2
2
  from threading import Event, Lock, Thread
3
- from typing import Dict, Optional
3
+ from typing import TYPE_CHECKING, Dict, Optional
4
4
 
5
5
  from opentelemetry.context import Context
6
- from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
7
- from opentelemetry.sdk.trace.export import SpanExporter
6
+ from opentelemetry.sdk.trace import Span, SpanProcessor
7
+
8
+ if TYPE_CHECKING:
9
+ from opentelemetry.sdk.trace import ReadableSpan, Span
10
+ from opentelemetry.sdk.trace.export import SpanExporter
8
11
 
9
12
 
10
13
  class InFlightSpanProcessor(SpanProcessor):
11
- def __init__(self, span_exporter: SpanExporter):
14
+ def __init__(self, span_exporter: "SpanExporter"):
12
15
  self.span_exporter = span_exporter
13
16
  self._in_flight: Dict[int, Span] = {}
14
17
  self._lock = Lock()
@@ -26,7 +29,7 @@ class InFlightSpanProcessor(SpanProcessor):
26
29
  if to_export:
27
30
  self.span_exporter.export(to_export)
28
31
 
29
- def _readable_span(self, span: Span) -> ReadableSpan:
32
+ def _readable_span(self, span: "Span") -> "ReadableSpan":
30
33
  readable = span._readable_span()
31
34
  readable._end_time = time.time_ns()
32
35
  readable._attributes = {
@@ -35,13 +38,13 @@ class InFlightSpanProcessor(SpanProcessor):
35
38
  }
36
39
  return readable
37
40
 
38
- def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
41
+ def on_start(self, span: "Span", parent_context: Optional[Context] = None) -> None:
39
42
  if not span.context or not span.context.trace_flags.sampled:
40
43
  return
41
44
  with self._lock:
42
45
  self._in_flight[span.context.span_id] = span
43
46
 
44
- def on_end(self, span: ReadableSpan) -> None:
47
+ def on_end(self, span: "ReadableSpan") -> None:
45
48
  if not span.context or not span.context.trace_flags.sampled:
46
49
  return
47
50
  with self._lock:
@@ -0,0 +1,231 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+ from typing import TYPE_CHECKING, Any, Optional, Union
4
+
5
+ from opentelemetry import propagate, trace
6
+ from opentelemetry.context import Context
7
+ from opentelemetry.propagators.textmap import Setter
8
+ from opentelemetry.trace import (
9
+ Span,
10
+ Status,
11
+ StatusCode,
12
+ get_tracer,
13
+ )
14
+ from typing_extensions import TypeAlias
15
+
16
+ import prefect
17
+ from prefect.client.orchestration import PrefectClient, SyncPrefectClient
18
+ from prefect.client.schemas import FlowRun, TaskRun
19
+ from prefect.client.schemas.objects import State
20
+ from prefect.context import FlowRunContext, TaskRunContext
21
+ from prefect.types import KeyValueLabels
22
+
23
+ if TYPE_CHECKING:
24
+ from opentelemetry.trace import Tracer
25
+
26
+ LABELS_TRACEPARENT_KEY = "__OTEL_TRACEPARENT"
27
+ TRACEPARENT_KEY = "traceparent"
28
+
29
+ FlowOrTaskRun: TypeAlias = Union[FlowRun, TaskRun]
30
+
31
+
32
+ class OTELSetter(Setter[KeyValueLabels]):
33
+ """
34
+ A setter for OpenTelemetry that supports Prefect's custom labels.
35
+ """
36
+
37
+ def set(self, carrier: KeyValueLabels, key: str, value: str) -> None:
38
+ carrier[key] = value
39
+
40
+
41
+ @dataclass
42
+ class RunTelemetry:
43
+ """
44
+ A class for managing the telemetry of runs.
45
+ """
46
+
47
+ _tracer: "Tracer" = field(
48
+ default_factory=lambda: get_tracer("prefect", prefect.__version__)
49
+ )
50
+ span: Optional[Span] = None
51
+
52
+ async def async_start_span(
53
+ self,
54
+ run: FlowOrTaskRun,
55
+ client: PrefectClient,
56
+ name: Optional[str] = None,
57
+ parameters: Optional[dict[str, Any]] = None,
58
+ ):
59
+ traceparent, span = self._start_span(run, name, parameters)
60
+
61
+ if self._run_type(run) == "flow" and traceparent:
62
+ # Only explicitly update labels if the run is a flow as task runs
63
+ # are updated via events.
64
+ await client.update_flow_run_labels(
65
+ run.id, {LABELS_TRACEPARENT_KEY: traceparent}
66
+ )
67
+
68
+ return span
69
+
70
+ def start_span(
71
+ self,
72
+ run: FlowOrTaskRun,
73
+ client: SyncPrefectClient,
74
+ name: Optional[str] = None,
75
+ parameters: Optional[dict[str, Any]] = None,
76
+ ):
77
+ traceparent, span = self._start_span(run, name, parameters)
78
+
79
+ if self._run_type(run) == "flow" and traceparent:
80
+ # Only explicitly update labels if the run is a flow as task runs
81
+ # are updated via events.
82
+ client.update_flow_run_labels(run.id, {LABELS_TRACEPARENT_KEY: traceparent})
83
+
84
+ return span
85
+
86
+ def _start_span(
87
+ self,
88
+ run: FlowOrTaskRun,
89
+ name: Optional[str] = None,
90
+ parameters: Optional[dict[str, Any]] = None,
91
+ ) -> tuple[Optional[str], Span]:
92
+ """
93
+ Start a span for a run.
94
+ """
95
+ if parameters is None:
96
+ parameters = {}
97
+
98
+ parameter_attributes = {
99
+ f"prefect.run.parameter.{k}": type(v).__name__
100
+ for k, v in parameters.items()
101
+ }
102
+
103
+ # Use existing trace context if this run already has one (e.g., from
104
+ # server operations like Late), otherwise use parent's trace context if
105
+ # available (e.g., nested flow / task runs). If neither exists, this
106
+ # will be a root span (e.g., a top-level flow run).
107
+ if LABELS_TRACEPARENT_KEY in run.labels:
108
+ context = self._trace_context_from_labels(run.labels)
109
+ else:
110
+ parent_run = self._parent_run()
111
+ parent_labels = parent_run.labels if parent_run else {}
112
+ if LABELS_TRACEPARENT_KEY in parent_labels:
113
+ context = self._trace_context_from_labels(parent_labels)
114
+ else:
115
+ context = None
116
+
117
+ run_type = self._run_type(run)
118
+
119
+ self.span = self._tracer.start_span(
120
+ name=name or run.name,
121
+ context=context,
122
+ attributes={
123
+ "prefect.run.name": name or run.name,
124
+ "prefect.run.type": run_type,
125
+ "prefect.run.id": str(run.id),
126
+ "prefect.tags": run.tags,
127
+ **parameter_attributes,
128
+ **{
129
+ key: value
130
+ for key, value in run.labels.items()
131
+ if not key.startswith("__") # exclude internal labels
132
+ },
133
+ },
134
+ )
135
+
136
+ if traceparent := self._traceparent_from_span(self.span):
137
+ run.labels[LABELS_TRACEPARENT_KEY] = traceparent
138
+
139
+ return traceparent, self.span
140
+
141
+ def _run_type(self, run: FlowOrTaskRun) -> str:
142
+ return "task" if isinstance(run, TaskRun) else "flow"
143
+
144
+ def _trace_context_from_labels(
145
+ self, labels: Optional[KeyValueLabels]
146
+ ) -> Optional[Context]:
147
+ """Get trace context from run labels if it exists."""
148
+ if not labels or LABELS_TRACEPARENT_KEY not in labels:
149
+ return None
150
+ traceparent = labels[LABELS_TRACEPARENT_KEY]
151
+ carrier = {TRACEPARENT_KEY: traceparent}
152
+ return propagate.extract(carrier)
153
+
154
+ def _traceparent_from_span(self, span: Span) -> Optional[str]:
155
+ carrier = {}
156
+ propagate.inject(carrier, context=trace.set_span_in_context(span))
157
+ return carrier.get(TRACEPARENT_KEY)
158
+
159
+ def end_span_on_success(self) -> None:
160
+ """
161
+ End a span for a run on success.
162
+ """
163
+ if self.span:
164
+ self.span.set_status(Status(StatusCode.OK))
165
+ self.span.end(time.time_ns())
166
+ self.span = None
167
+
168
+ def end_span_on_failure(self, terminal_message: Optional[str] = None) -> None:
169
+ """
170
+ End a span for a run on failure.
171
+ """
172
+ if self.span:
173
+ self.span.set_status(
174
+ Status(StatusCode.ERROR, terminal_message or "Run failed")
175
+ )
176
+ self.span.end(time.time_ns())
177
+ self.span = None
178
+
179
+ def record_exception(self, exc: BaseException) -> None:
180
+ """
181
+ Record an exception on a span.
182
+ """
183
+ if self.span:
184
+ self.span.record_exception(exc)
185
+
186
+ def update_state(self, new_state: State) -> None:
187
+ """
188
+ Update a span with the state of a run.
189
+ """
190
+ if self.span:
191
+ self.span.add_event(
192
+ new_state.name or new_state.type,
193
+ {
194
+ "prefect.state.message": new_state.message or "",
195
+ "prefect.state.type": new_state.type,
196
+ "prefect.state.name": new_state.name or new_state.type,
197
+ "prefect.state.id": str(new_state.id),
198
+ },
199
+ )
200
+
201
+ def _parent_run(self) -> Union[FlowOrTaskRun, None]:
202
+ """
203
+ Identify the "parent run" for the current execution context.
204
+
205
+ Both flows and tasks can be nested "infinitely," and each creates a
206
+ corresponding context when executed. This method determines the most
207
+ appropriate parent context (either a task run or a flow run) based on
208
+ their relationship in the current hierarchy.
209
+
210
+ Returns:
211
+ FlowOrTaskRun: The parent run object (task or flow) if applicable.
212
+ None: If there is no parent context, implying the current run is the top-level parent.
213
+ """
214
+ parent_flow_run_context = FlowRunContext.get()
215
+ parent_task_run_context = TaskRunContext.get()
216
+
217
+ if parent_task_run_context and parent_flow_run_context:
218
+ # If both contexts exist, which is common for nested flows or tasks,
219
+ # check if the task's flow_run_id matches the current flow_run.
220
+ # If they match, the task is a child of the flow and is the parent of the current run.
221
+ flow_run_id = getattr(parent_flow_run_context.flow_run, "id", None)
222
+ if parent_task_run_context.task_run.flow_run_id == flow_run_id:
223
+ return parent_task_run_context.task_run
224
+ # Otherwise, assume the flow run is the entry point and is the parent.
225
+ return parent_flow_run_context.flow_run
226
+ elif parent_flow_run_context:
227
+ return parent_flow_run_context.flow_run
228
+ elif parent_task_run_context:
229
+ return parent_task_run_context.task_run
230
+
231
+ return None
prefect/transactions.py CHANGED
@@ -23,7 +23,7 @@ from prefect.exceptions import (
23
23
  MissingContextError,
24
24
  SerializationError,
25
25
  )
26
- from prefect.logging.loggers import get_logger, get_run_logger
26
+ from prefect.logging.loggers import LoggingAdapter, get_logger, get_run_logger
27
27
  from prefect.records import RecordStore
28
28
  from prefect.records.base import TransactionRecord
29
29
  from prefect.results import (
@@ -32,9 +32,9 @@ from prefect.results import (
32
32
  ResultStore,
33
33
  get_result_store,
34
34
  )
35
+ from prefect.utilities._engine import get_hook_name
35
36
  from prefect.utilities.annotations import NotSet
36
37
  from prefect.utilities.collections import AutoEnum
37
- from prefect.utilities.engine import _get_hook_name
38
38
 
39
39
 
40
40
  class IsolationLevel(AutoEnum):
@@ -72,13 +72,13 @@ class Transaction(ContextModel):
72
72
  default_factory=list
73
73
  )
74
74
  overwrite: bool = False
75
- logger: Union[logging.Logger, logging.LoggerAdapter] = Field(
75
+ logger: Union[logging.Logger, LoggingAdapter] = Field(
76
76
  default_factory=partial(get_logger, "transactions")
77
77
  )
78
78
  write_on_commit: bool = True
79
79
  _stored_values: Dict[str, Any] = PrivateAttr(default_factory=dict)
80
80
  _staged_value: Any = None
81
- __var__: ContextVar = ContextVar("transaction")
81
+ __var__: ContextVar[Self] = ContextVar("transaction")
82
82
 
83
83
  def set(self, name: str, value: Any) -> None:
84
84
  """
@@ -209,7 +209,7 @@ class Transaction(ContextModel):
209
209
  self._token = self.__var__.set(self)
210
210
  return self
211
211
 
212
- def __exit__(self, *exc_info):
212
+ def __exit__(self, *exc_info: Any):
213
213
  exc_type, exc_val, _ = exc_info
214
214
  if not self._token:
215
215
  raise RuntimeError(
@@ -254,7 +254,7 @@ class Transaction(ContextModel):
254
254
  ):
255
255
  self.state = TransactionState.COMMITTED
256
256
 
257
- def read(self) -> Union["BaseResult", ResultRecord, None]:
257
+ def read(self) -> Union["BaseResult[Any]", ResultRecord[Any], None]:
258
258
  if self.store and self.key:
259
259
  record = self.store.read(key=self.key)
260
260
  if isinstance(record, ResultRecord):
@@ -354,8 +354,8 @@ class Transaction(ContextModel):
354
354
  self.rollback()
355
355
  return False
356
356
 
357
- def run_hook(self, hook, hook_type: str) -> None:
358
- hook_name = _get_hook_name(hook)
357
+ def run_hook(self, hook: Callable[..., Any], hook_type: str) -> None:
358
+ hook_name = get_hook_name(hook)
359
359
  # Undocumented way to disable logging for a hook. Subject to change.
360
360
  should_log = getattr(hook, "log_on_run", True)
361
361
 
@@ -379,8 +379,8 @@ class Transaction(ContextModel):
379
379
  def stage(
380
380
  self,
381
381
  value: Any,
382
- on_rollback_hooks: Optional[List] = None,
383
- on_commit_hooks: Optional[List] = None,
382
+ on_rollback_hooks: Optional[list[Callable[..., Any]]] = None,
383
+ on_commit_hooks: Optional[list[Callable[..., Any]]] = None,
384
384
  ) -> None:
385
385
  """
386
386
  Stage a value to be committed later.
@@ -441,7 +441,7 @@ def transaction(
441
441
  isolation_level: Optional[IsolationLevel] = None,
442
442
  overwrite: bool = False,
443
443
  write_on_commit: bool = True,
444
- logger: Union[logging.Logger, logging.LoggerAdapter, None] = None,
444
+ logger: Optional[Union[logging.Logger, LoggingAdapter]] = None,
445
445
  ) -> Generator[Transaction, None, None]:
446
446
  """
447
447
  A context manager for opening and managing a transaction.
@@ -465,9 +465,9 @@ def transaction(
465
465
  store = get_result_store()
466
466
 
467
467
  try:
468
- logger = logger or get_run_logger()
468
+ _logger: Union[logging.Logger, LoggingAdapter] = logger or get_run_logger()
469
469
  except MissingContextError:
470
- logger = get_logger("transactions")
470
+ _logger = get_logger("transactions")
471
471
 
472
472
  with Transaction(
473
473
  key=key,
@@ -476,6 +476,6 @@ def transaction(
476
476
  isolation_level=isolation_level,
477
477
  overwrite=overwrite,
478
478
  write_on_commit=write_on_commit,
479
- logger=logger,
479
+ logger=_logger,
480
480
  ) as txn:
481
481
  yield txn
prefect/types/__init__.py CHANGED
@@ -3,7 +3,8 @@ from typing import Annotated, Any, Dict, List, Optional, Set, TypeVar, Union
3
3
  from typing_extensions import Literal, TypeAlias
4
4
  import orjson
5
5
  import pydantic
6
-
6
+ from pydantic_extra_types.pendulum_dt import DateTime as PydanticDateTime
7
+ from pydantic_extra_types.pendulum_dt import Date as PydanticDate
7
8
  from pydantic import (
8
9
  BeforeValidator,
9
10
  Field,
@@ -34,6 +35,8 @@ TimeZone = Annotated[
34
35
  ),
35
36
  ]
36
37
 
38
+ DateTime: TypeAlias = PydanticDateTime
39
+ Date: TypeAlias = PydanticDate
37
40
 
38
41
  BANNED_CHARACTERS = ["/", "%", "&", ">", "<"]
39
42
 
@@ -96,7 +99,7 @@ def cast_none_to_empty_dict(value: Any) -> dict[str, Any]:
96
99
 
97
100
 
98
101
  KeyValueLabels = Annotated[
99
- dict[str, Union[StrictBool, StrictInt, StrictFloat, str]],
102
+ Dict[str, Union[StrictBool, StrictInt, StrictFloat, str]],
100
103
  BeforeValidator(cast_none_to_empty_dict),
101
104
  ]
102
105
 
@@ -149,9 +152,6 @@ LogLevel = Annotated[
149
152
  ]
150
153
 
151
154
 
152
- KeyValueLabels: TypeAlias = dict[str, Union[StrictBool, StrictInt, StrictFloat, str]]
153
-
154
-
155
155
  def convert_none_to_empty_dict(v: Optional[KeyValueLabels]) -> KeyValueLabels:
156
156
  return v or {}
157
157
 
@@ -0,0 +1,96 @@
1
+ """Internal engine utilities"""
2
+
3
+
4
+ from collections.abc import Callable
5
+ from functools import partial
6
+ from typing import TYPE_CHECKING, Any, Union
7
+ from uuid import uuid4
8
+
9
+ from prefect.context import FlowRunContext
10
+ from prefect.flows import Flow
11
+ from prefect.tasks import Task, TaskRunNameCallbackWithParameters
12
+
13
+
14
+ def dynamic_key_for_task_run(
15
+ context: FlowRunContext, task: "Task[..., Any]", stable: bool = True
16
+ ) -> Union[int, str]:
17
+ if (
18
+ stable is False or context.detached
19
+ ): # this task is running on remote infrastructure
20
+ return str(uuid4())
21
+ elif context.flow_run is None: # this is an autonomous task run
22
+ context.task_run_dynamic_keys[task.task_key] = getattr(
23
+ task, "dynamic_key", str(uuid4())
24
+ )
25
+
26
+ elif task.task_key not in context.task_run_dynamic_keys:
27
+ context.task_run_dynamic_keys[task.task_key] = 0
28
+ else:
29
+ dynamic_key = context.task_run_dynamic_keys[task.task_key]
30
+ if TYPE_CHECKING:
31
+ assert isinstance(dynamic_key, int)
32
+ context.task_run_dynamic_keys[task.task_key] = dynamic_key + 1
33
+
34
+ return context.task_run_dynamic_keys[task.task_key]
35
+
36
+
37
+ def resolve_custom_flow_run_name(
38
+ flow: "Flow[..., Any]", parameters: dict[str, Any]
39
+ ) -> str:
40
+ if callable(flow.flow_run_name):
41
+ flow_run_name = flow.flow_run_name()
42
+ if not TYPE_CHECKING:
43
+ if not isinstance(flow_run_name, str):
44
+ raise TypeError(
45
+ f"Callable {flow.flow_run_name} for 'flow_run_name' returned type"
46
+ f" {type(flow_run_name).__name__} but a string is required."
47
+ )
48
+ elif isinstance(flow.flow_run_name, str):
49
+ flow_run_name = flow.flow_run_name.format(**parameters)
50
+ else:
51
+ raise TypeError(
52
+ "Expected string or callable for 'flow_run_name'; got"
53
+ f" {type(flow.flow_run_name).__name__} instead."
54
+ )
55
+
56
+ return flow_run_name
57
+
58
+
59
+ def resolve_custom_task_run_name(
60
+ task: "Task[..., Any]", parameters: dict[str, Any]
61
+ ) -> str:
62
+ if callable(task.task_run_name):
63
+ # If the callable accepts a 'parameters' kwarg, pass the entire parameters dict
64
+ if TaskRunNameCallbackWithParameters.is_callback_with_parameters(
65
+ task.task_run_name
66
+ ):
67
+ task_run_name = task.task_run_name(parameters=parameters)
68
+ else:
69
+ # If it doesn't expect parameters, call it without arguments
70
+ task_run_name = task.task_run_name()
71
+
72
+ if not TYPE_CHECKING:
73
+ if not isinstance(task_run_name, str):
74
+ raise TypeError(
75
+ f"Callable {task.task_run_name} for 'task_run_name' returned type"
76
+ f" {type(task_run_name).__name__} but a string is required."
77
+ )
78
+ elif isinstance(task.task_run_name, str):
79
+ task_run_name = task.task_run_name.format(**parameters)
80
+ else:
81
+ raise TypeError(
82
+ "Expected string or callable for 'task_run_name'; got"
83
+ f" {type(task.task_run_name).__name__} instead."
84
+ )
85
+
86
+ return task_run_name
87
+
88
+
89
+ def get_hook_name(hook: Callable[..., Any]) -> str:
90
+ return (
91
+ hook.__name__
92
+ if hasattr(hook, "__name__")
93
+ else (
94
+ hook.func.__name__ if isinstance(hook, partial) else hook.__class__.__name__
95
+ )
96
+ )
@@ -1,33 +1,40 @@
1
1
  import warnings
2
- from abc import ABC
3
- from collections import namedtuple
4
- from typing import Generic, TypeVar
2
+ from operator import itemgetter
3
+ from typing import Any, cast
5
4
 
6
- T = TypeVar("T")
5
+ from typing_extensions import Self, TypeVar
7
6
 
7
+ T = TypeVar("T", infer_variance=True)
8
8
 
9
- class BaseAnnotation(
10
- namedtuple("BaseAnnotation", field_names="value"), ABC, Generic[T]
11
- ):
9
+
10
+ class BaseAnnotation(tuple[T]):
12
11
  """
13
12
  Base class for Prefect annotation types.
14
13
 
15
- Inherits from `namedtuple` for unpacking support in another tools.
14
+ Inherits from `tuple` for unpacking support in other tools.
16
15
  """
17
16
 
17
+ __slots__ = ()
18
+
19
+ def __new__(cls, value: T) -> Self:
20
+ return super().__new__(cls, (value,))
21
+
22
+ # use itemgetter to minimise overhead, just like namedtuple generated code would
23
+ value: T = cast(T, property(itemgetter(0)))
24
+
18
25
  def unwrap(self) -> T:
19
- return self.value
26
+ return self[0]
20
27
 
21
- def rewrap(self, value: T) -> "BaseAnnotation[T]":
28
+ def rewrap(self, value: T) -> Self:
22
29
  return type(self)(value)
23
30
 
24
- def __eq__(self, other: "BaseAnnotation[T]") -> bool:
31
+ def __eq__(self, other: Any) -> bool:
25
32
  if type(self) is not type(other):
26
33
  return False
27
- return self.unwrap() == other.unwrap()
34
+ return super().__eq__(other)
28
35
 
29
36
  def __repr__(self) -> str:
30
- return f"{type(self).__name__}({self.value!r})"
37
+ return f"{type(self).__name__}({self[0]!r})"
31
38
 
32
39
 
33
40
  class unmapped(BaseAnnotation[T]):
@@ -38,9 +45,9 @@ class unmapped(BaseAnnotation[T]):
38
45
  operation instead of being split.
39
46
  """
40
47
 
41
- def __getitem__(self, _) -> T:
48
+ def __getitem__(self, _: object) -> T: # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
42
49
  # Internally, this acts as an infinite array where all items are the same value
43
- return self.unwrap()
50
+ return super().__getitem__(0)
44
51
 
45
52
 
46
53
  class allow_failure(BaseAnnotation[T]):
@@ -87,14 +94,14 @@ class quote(BaseAnnotation[T]):
87
94
 
88
95
 
89
96
  # Backwards compatibility stub for `Quote` class
90
- class Quote(quote):
91
- def __init__(self, expr):
97
+ class Quote(quote[T]):
98
+ def __new__(cls, expr: T) -> Self:
92
99
  warnings.warn(
93
100
  "Use of `Quote` is deprecated. Use `quote` instead.",
94
101
  DeprecationWarning,
95
102
  stacklevel=2,
96
103
  )
97
- super().__init__(expr)
104
+ return super().__new__(cls, expr)
98
105
 
99
106
 
100
107
  class NotSet: