prefect-client 3.1.6__py3-none-any.whl → 3.1.8__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 (55) hide show
  1. prefect/_experimental/__init__.py +0 -0
  2. prefect/_experimental/lineage.py +181 -0
  3. prefect/_internal/compatibility/async_dispatch.py +38 -9
  4. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  5. prefect/_internal/retries.py +15 -6
  6. prefect/_internal/schemas/bases.py +2 -1
  7. prefect/_internal/schemas/validators.py +5 -4
  8. prefect/_version.py +3 -3
  9. prefect/blocks/core.py +144 -17
  10. prefect/blocks/system.py +2 -1
  11. prefect/client/orchestration.py +106 -0
  12. prefect/client/schemas/actions.py +5 -5
  13. prefect/client/schemas/filters.py +1 -1
  14. prefect/client/schemas/objects.py +5 -5
  15. prefect/client/schemas/responses.py +1 -2
  16. prefect/client/schemas/schedules.py +1 -1
  17. prefect/client/subscriptions.py +2 -1
  18. prefect/client/utilities.py +15 -1
  19. prefect/context.py +1 -1
  20. prefect/deployments/flow_runs.py +3 -3
  21. prefect/deployments/runner.py +14 -14
  22. prefect/deployments/steps/core.py +3 -1
  23. prefect/deployments/steps/pull.py +60 -12
  24. prefect/events/clients.py +55 -4
  25. prefect/events/filters.py +1 -1
  26. prefect/events/related.py +2 -1
  27. prefect/events/schemas/events.py +1 -1
  28. prefect/events/utilities.py +2 -0
  29. prefect/events/worker.py +8 -0
  30. prefect/flow_engine.py +41 -81
  31. prefect/flow_runs.py +4 -2
  32. prefect/flows.py +4 -6
  33. prefect/results.py +43 -22
  34. prefect/runner/runner.py +129 -18
  35. prefect/runner/storage.py +3 -3
  36. prefect/serializers.py +28 -24
  37. prefect/settings/__init__.py +1 -0
  38. prefect/settings/base.py +3 -2
  39. prefect/settings/models/api.py +4 -0
  40. prefect/settings/models/experiments.py +5 -0
  41. prefect/settings/models/runner.py +8 -0
  42. prefect/settings/models/server/api.py +7 -1
  43. prefect/task_engine.py +34 -26
  44. prefect/task_worker.py +43 -25
  45. prefect/tasks.py +118 -125
  46. prefect/telemetry/instrumentation.py +1 -1
  47. prefect/telemetry/processors.py +10 -7
  48. prefect/telemetry/run_telemetry.py +157 -33
  49. prefect/types/__init__.py +4 -1
  50. prefect/variables.py +127 -19
  51. {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/METADATA +2 -1
  52. {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/RECORD +55 -53
  53. {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/LICENSE +0 -0
  54. {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/WHEEL +0 -0
  55. {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,33 @@
1
1
  import time
2
2
  from dataclasses import dataclass, field
3
- from typing import TYPE_CHECKING, Any, Dict, Optional
3
+ from typing import TYPE_CHECKING, Any, Optional, Union
4
4
 
5
+ from opentelemetry import propagate, trace
6
+ from opentelemetry.context import Context
5
7
  from opentelemetry.propagators.textmap import Setter
6
8
  from opentelemetry.trace import (
9
+ Span,
7
10
  Status,
8
11
  StatusCode,
9
12
  get_tracer,
10
13
  )
14
+ from typing_extensions import TypeAlias
11
15
 
12
16
  import prefect
13
- from prefect.client.schemas import TaskRun
17
+ from prefect.client.orchestration import PrefectClient, SyncPrefectClient
18
+ from prefect.client.schemas import FlowRun, TaskRun
14
19
  from prefect.client.schemas.objects import State
20
+ from prefect.context import FlowRunContext, TaskRunContext
15
21
  from prefect.types import KeyValueLabels
16
22
 
17
23
  if TYPE_CHECKING:
18
24
  from opentelemetry.trace import Tracer
19
25
 
26
+ LABELS_TRACEPARENT_KEY = "__OTEL_TRACEPARENT"
27
+ TRACEPARENT_KEY = "traceparent"
28
+
29
+ FlowOrTaskRun: TypeAlias = Union[FlowRun, TaskRun]
30
+
20
31
 
21
32
  class OTELSetter(Setter[KeyValueLabels]):
22
33
  """
@@ -36,67 +47,148 @@ class RunTelemetry:
36
47
  _tracer: "Tracer" = field(
37
48
  default_factory=lambda: get_tracer("prefect", prefect.__version__)
38
49
  )
39
- _span = None
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
40
69
 
41
70
  def start_span(
42
71
  self,
43
- task_run: TaskRun,
44
- parameters: Optional[Dict[str, Any]] = None,
45
- labels: Optional[Dict[str, Any]] = None,
72
+ run: FlowOrTaskRun,
73
+ client: SyncPrefectClient,
74
+ name: Optional[str] = None,
75
+ parameters: Optional[dict[str, Any]] = None,
46
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]:
47
92
  """
48
- Start a span for a task run.
93
+ Start a span for a run.
49
94
  """
50
95
  if parameters is None:
51
96
  parameters = {}
52
- if labels is None:
53
- labels = {}
97
+
54
98
  parameter_attributes = {
55
99
  f"prefect.run.parameter.{k}": type(v).__name__
56
100
  for k, v in parameters.items()
57
101
  }
58
- self._span = self._tracer.start_span(
59
- name=task_run.name,
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,
60
122
  attributes={
61
- "prefect.run.type": "task",
62
- "prefect.run.id": str(task_run.id),
63
- "prefect.tags": task_run.tags,
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,
64
127
  **parameter_attributes,
65
- **labels,
128
+ **{
129
+ key: value
130
+ for key, value in run.labels.items()
131
+ if not key.startswith("__") # exclude internal labels
132
+ },
66
133
  },
67
134
  )
68
135
 
69
- def end_span_on_success(self, terminal_message: str) -> None:
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:
70
160
  """
71
- End a span for a task run on success.
161
+ End a span for a run on success.
72
162
  """
73
- if self._span:
74
- self._span.set_status(Status(StatusCode.OK), terminal_message)
75
- self._span.end(time.time_ns())
76
- self._span = None
163
+ if self.span:
164
+ self.span.set_status(Status(StatusCode.OK))
165
+ self.span.end(time.time_ns())
166
+ self.span = None
77
167
 
78
- def end_span_on_failure(self, terminal_message: str) -> None:
168
+ def end_span_on_failure(self, terminal_message: Optional[str] = None) -> None:
79
169
  """
80
- End a span for a task run on failure.
170
+ End a span for a run on failure.
81
171
  """
82
- if self._span:
83
- self._span.set_status(Status(StatusCode.ERROR, terminal_message))
84
- self._span.end(time.time_ns())
85
- self._span = None
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
86
178
 
87
- def record_exception(self, exc: Exception) -> None:
179
+ def record_exception(self, exc: BaseException) -> None:
88
180
  """
89
181
  Record an exception on a span.
90
182
  """
91
- if self._span:
92
- self._span.record_exception(exc)
183
+ if self.span:
184
+ self.span.record_exception(exc)
93
185
 
94
186
  def update_state(self, new_state: State) -> None:
95
187
  """
96
- Update a span with the state of a task run.
188
+ Update a span with the state of a run.
97
189
  """
98
- if self._span:
99
- self._span.add_event(
190
+ if self.span:
191
+ self.span.add_event(
100
192
  new_state.name or new_state.type,
101
193
  {
102
194
  "prefect.state.message": new_state.message or "",
@@ -105,3 +197,35 @@ class RunTelemetry:
105
197
  "prefect.state.id": str(new_state.id),
106
198
  },
107
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/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
 
prefect/variables.py CHANGED
@@ -1,13 +1,14 @@
1
- from typing import List, Optional
1
+ from typing import Optional
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
+ from prefect._internal.compatibility.async_dispatch import async_dispatch
5
6
  from prefect._internal.compatibility.migration import getattr_migration
7
+ from prefect.client.orchestration import get_client
6
8
  from prefect.client.schemas.actions import VariableCreate, VariableUpdate
7
9
  from prefect.client.utilities import get_or_create_client
8
10
  from prefect.exceptions import ObjectNotFound
9
11
  from prefect.types import MAX_VARIABLE_NAME_LENGTH, StrictVariableValue
10
- from prefect.utilities.asyncutils import sync_compatible
11
12
 
12
13
 
13
14
  class Variable(BaseModel):
@@ -31,19 +32,18 @@ class Variable(BaseModel):
31
32
  description="The value of the variable",
32
33
  examples=["my-value"],
33
34
  )
34
- tags: Optional[List[str]] = Field(default=None)
35
+ tags: Optional[list[str]] = Field(default=None)
35
36
 
36
37
  @classmethod
37
- @sync_compatible
38
- async def set(
38
+ async def aset(
39
39
  cls,
40
40
  name: str,
41
41
  value: StrictVariableValue,
42
- tags: Optional[List[str]] = None,
42
+ tags: Optional[list[str]] = None,
43
43
  overwrite: bool = False,
44
44
  ) -> "Variable":
45
45
  """
46
- Sets a new variable. If one exists with the same name, must pass `overwrite=True`
46
+ Asynchronously sets a new variable. If one exists with the same name, must pass `overwrite=True`
47
47
 
48
48
  Returns the newly set variable object.
49
49
 
@@ -60,8 +60,8 @@ class Variable(BaseModel):
60
60
  from prefect.variables import Variable
61
61
 
62
62
  @flow
63
- def my_flow():
64
- Variable.set(name="my_var",value="test_value", tags=["hi", "there"], overwrite=True)
63
+ async def my_flow():
64
+ await Variable.aset(name="my_var",value="test_value", tags=["hi", "there"], overwrite=True)
65
65
  ```
66
66
  """
67
67
  client, _ = get_or_create_client()
@@ -87,14 +87,62 @@ class Variable(BaseModel):
87
87
  return cls.model_validate(var_dict)
88
88
 
89
89
  @classmethod
90
- @sync_compatible
91
- async def get(
90
+ @async_dispatch(aset)
91
+ def set(
92
+ cls,
93
+ name: str,
94
+ value: StrictVariableValue,
95
+ tags: Optional[list[str]] = None,
96
+ overwrite: bool = False,
97
+ ) -> "Variable":
98
+ """
99
+ Sets a new variable. If one exists with the same name, must pass `overwrite=True`
100
+
101
+ Returns the newly set variable object.
102
+
103
+ Args:
104
+ - name: The name of the variable to set.
105
+ - value: The value of the variable to set.
106
+ - tags: An optional list of strings to associate with the variable.
107
+ - overwrite: Whether to overwrite the variable if it already exists.
108
+
109
+ Example:
110
+ Set a new variable and overwrite it if it already exists.
111
+
112
+ ```
113
+ from prefect.variables import Variable
114
+
115
+ @flow
116
+ def my_flow():
117
+ Variable.set(name="my_var",value="test_value", tags=["hi", "there"], overwrite=True)
118
+ ```
119
+ """
120
+ with get_client(sync_client=True) as client:
121
+ variable_exists = client.read_variable_by_name(name)
122
+ var_dict = {"name": name, "value": value, "tags": tags or []}
123
+
124
+ if variable_exists:
125
+ if not overwrite:
126
+ raise ValueError(
127
+ f"Variable {name!r} already exists. Use `overwrite=True` to update it."
128
+ )
129
+ client.update_variable(variable=VariableUpdate.model_validate(var_dict))
130
+ variable = client.read_variable_by_name(name)
131
+ for key in var_dict.keys():
132
+ var_dict.update({key: getattr(variable, key)})
133
+ else:
134
+ client.create_variable(variable=VariableCreate.model_validate(var_dict))
135
+
136
+ return cls.model_validate(var_dict)
137
+
138
+ @classmethod
139
+ async def aget(
92
140
  cls,
93
141
  name: str,
94
142
  default: StrictVariableValue = None,
95
143
  ) -> StrictVariableValue:
96
144
  """
97
- Get a variable's value by name.
145
+ Asynchronously get a variable's value by name.
98
146
 
99
147
  If the variable does not exist, return the default value.
100
148
 
@@ -109,8 +157,8 @@ class Variable(BaseModel):
109
157
  from prefect.variables import Variable
110
158
 
111
159
  @flow
112
- def my_flow():
113
- var = Variable.get("my_var")
160
+ async def my_flow():
161
+ var = await Variable.aget("my_var")
114
162
  ```
115
163
  """
116
164
  client, _ = get_or_create_client()
@@ -119,10 +167,41 @@ class Variable(BaseModel):
119
167
  return variable.value if variable else default
120
168
 
121
169
  @classmethod
122
- @sync_compatible
123
- async def unset(cls, name: str) -> bool:
170
+ @async_dispatch(aget)
171
+ def get(
172
+ cls,
173
+ name: str,
174
+ default: StrictVariableValue = None,
175
+ ) -> StrictVariableValue:
124
176
  """
125
- Unset a variable by name.
177
+ Get a variable's value by name.
178
+
179
+ If the variable does not exist, return the default value.
180
+
181
+ Args:
182
+ - name: The name of the variable value to get.
183
+ - default: The default value to return if the variable does not exist.
184
+
185
+ Example:
186
+ Get a variable's value by name.
187
+ ```python
188
+ from prefect import flow
189
+ from prefect.variables import Variable
190
+
191
+ @flow
192
+ def my_flow():
193
+ var = Variable.get("my_var")
194
+ ```
195
+ """
196
+ with get_client(sync_client=True) as client:
197
+ variable = client.read_variable_by_name(name)
198
+
199
+ return variable.value if variable else default
200
+
201
+ @classmethod
202
+ async def aunset(cls, name: str) -> bool:
203
+ """
204
+ Asynchronously unset a variable by name.
126
205
 
127
206
  Args:
128
207
  - name: The name of the variable to unset.
@@ -136,8 +215,8 @@ class Variable(BaseModel):
136
215
  from prefect.variables import Variable
137
216
 
138
217
  @flow
139
- def my_flow():
140
- Variable.unset("my_var")
218
+ async def my_flow():
219
+ await Variable.aunset("my_var")
141
220
  ```
142
221
  """
143
222
  client, _ = get_or_create_client()
@@ -147,5 +226,34 @@ class Variable(BaseModel):
147
226
  except ObjectNotFound:
148
227
  return False
149
228
 
229
+ @classmethod
230
+ @async_dispatch(aunset)
231
+ def unset(cls, name: str) -> bool:
232
+ """
233
+ Unset a variable by name.
234
+
235
+ Args:
236
+ - name: The name of the variable to unset.
237
+
238
+ Returns `True` if the variable was deleted, `False` if the variable did not exist.
239
+
240
+ Example:
241
+ Unset a variable by name.
242
+ ```python
243
+ from prefect import flow
244
+ from prefect.variables import Variable
245
+
246
+ @flow
247
+ def my_flow():
248
+ Variable.unset("my_var")
249
+ ```
250
+ """
251
+ with get_client(sync_client=True) as client:
252
+ try:
253
+ client.delete_variable_by_name(name=name)
254
+ return True
255
+ except ObjectNotFound:
256
+ return False
257
+
150
258
 
151
259
  __getattr__ = getattr_migration(__name__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 3.1.6
3
+ Version: 3.1.8
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.
@@ -51,6 +51,7 @@ Requires-Dist: pydantic-extra-types<3.0.0,>=2.8.2
51
51
  Requires-Dist: pydantic-settings>2.2.1
52
52
  Requires-Dist: python-dateutil<3.0.0,>=2.8.2
53
53
  Requires-Dist: python-slugify<9.0,>=5.0
54
+ Requires-Dist: python-socks[asyncio]<3.0,>=2.5.3
54
55
  Requires-Dist: pyyaml<7.0.0,>=5.4.1
55
56
  Requires-Dist: rfc3339-validator<0.2.0,>=0.1.4
56
57
  Requires-Dist: rich<14.0,>=11.0