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.
- prefect/__init__.py +3 -0
- prefect/_experimental/__init__.py +0 -0
- prefect/_experimental/lineage.py +181 -0
- prefect/_internal/compatibility/async_dispatch.py +38 -9
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/pydantic/v2_validated_func.py +15 -10
- prefect/_internal/retries.py +15 -6
- prefect/_internal/schemas/bases.py +11 -8
- prefect/_internal/schemas/validators.py +7 -5
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +148 -19
- prefect/blocks/system.py +2 -1
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +430 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +128 -121
- prefect/client/schemas/filters.py +1 -1
- prefect/client/schemas/objects.py +114 -85
- prefect/client/schemas/responses.py +19 -20
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +30 -15
- prefect/client/utilities.py +46 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +40 -32
- prefect/deployments/flow_runs.py +6 -8
- prefect/deployments/runner.py +14 -14
- prefect/deployments/steps/core.py +3 -1
- prefect/deployments/steps/pull.py +60 -12
- prefect/docker/__init__.py +1 -1
- prefect/events/clients.py +55 -4
- prefect/events/filters.py +1 -1
- prefect/events/related.py +2 -1
- prefect/events/schemas/events.py +26 -21
- prefect/events/utilities.py +3 -2
- prefect/events/worker.py +8 -0
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +87 -87
- prefect/flow_runs.py +7 -5
- prefect/flows.py +218 -176
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/results.py +43 -22
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/storage.py +3 -3
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/serializers.py +28 -24
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/experiments.py +5 -0
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +87 -26
- prefect/task_runners.py +2 -2
- prefect/task_worker.py +43 -25
- prefect/tasks.py +148 -142
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/instrumentation.py +1 -1
- prefect/telemetry/processors.py +10 -7
- prefect/telemetry/run_telemetry.py +231 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +5 -5
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +136 -27
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
prefect/utilities/collections.py
CHANGED
@@ -6,33 +6,40 @@ import io
|
|
6
6
|
import itertools
|
7
7
|
import types
|
8
8
|
import warnings
|
9
|
-
from collections import OrderedDict
|
10
|
-
from collections.abc import
|
11
|
-
from collections.abc import Sequence
|
12
|
-
from dataclasses import fields, is_dataclass
|
13
|
-
from enum import Enum, auto
|
14
|
-
from typing import (
|
15
|
-
Any,
|
9
|
+
from collections import OrderedDict
|
10
|
+
from collections.abc import (
|
16
11
|
Callable,
|
17
|
-
|
12
|
+
Collection,
|
18
13
|
Generator,
|
19
14
|
Hashable,
|
20
15
|
Iterable,
|
21
|
-
|
22
|
-
|
16
|
+
Iterator,
|
17
|
+
Sequence,
|
23
18
|
Set,
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
)
|
20
|
+
from dataclasses import fields, is_dataclass, replace
|
21
|
+
from enum import Enum, auto
|
22
|
+
from typing import (
|
23
|
+
TYPE_CHECKING,
|
24
|
+
Any,
|
25
|
+
Literal,
|
26
|
+
Optional,
|
27
27
|
Union,
|
28
28
|
cast,
|
29
|
+
overload,
|
29
30
|
)
|
30
31
|
from unittest.mock import Mock
|
31
32
|
|
32
33
|
import pydantic
|
34
|
+
from typing_extensions import TypeAlias, TypeVar
|
33
35
|
|
34
36
|
# Quote moved to `prefect.utilities.annotations` but preserved here for compatibility
|
35
|
-
from prefect.utilities.annotations import BaseAnnotation
|
37
|
+
from prefect.utilities.annotations import BaseAnnotation as BaseAnnotation
|
38
|
+
from prefect.utilities.annotations import Quote as Quote
|
39
|
+
from prefect.utilities.annotations import quote as quote
|
40
|
+
|
41
|
+
if TYPE_CHECKING:
|
42
|
+
pass
|
36
43
|
|
37
44
|
|
38
45
|
class AutoEnum(str, Enum):
|
@@ -55,11 +62,12 @@ class AutoEnum(str, Enum):
|
|
55
62
|
```
|
56
63
|
"""
|
57
64
|
|
58
|
-
|
65
|
+
@staticmethod
|
66
|
+
def _generate_next_value_(name: str, *_: object, **__: object) -> str:
|
59
67
|
return name
|
60
68
|
|
61
69
|
@staticmethod
|
62
|
-
def auto():
|
70
|
+
def auto() -> str:
|
63
71
|
"""
|
64
72
|
Exposes `enum.auto()` to avoid requiring a second import to use `AutoEnum`
|
65
73
|
"""
|
@@ -70,12 +78,15 @@ class AutoEnum(str, Enum):
|
|
70
78
|
|
71
79
|
|
72
80
|
KT = TypeVar("KT")
|
73
|
-
VT = TypeVar("VT")
|
81
|
+
VT = TypeVar("VT", infer_variance=True)
|
82
|
+
VT1 = TypeVar("VT1", infer_variance=True)
|
83
|
+
VT2 = TypeVar("VT2", infer_variance=True)
|
84
|
+
R = TypeVar("R", infer_variance=True)
|
85
|
+
NestedDict: TypeAlias = dict[KT, Union[VT, "NestedDict[KT, VT]"]]
|
86
|
+
HashableT = TypeVar("HashableT", bound=Hashable)
|
74
87
|
|
75
88
|
|
76
|
-
def dict_to_flatdict(
|
77
|
-
dct: Dict[KT, Union[Any, Dict[KT, Any]]], _parent: Tuple[KT, ...] = None
|
78
|
-
) -> Dict[Tuple[KT, ...], Any]:
|
89
|
+
def dict_to_flatdict(dct: NestedDict[KT, VT]) -> dict[tuple[KT, ...], VT]:
|
79
90
|
"""Converts a (nested) dictionary to a flattened representation.
|
80
91
|
|
81
92
|
Each key of the flat dict will be a CompoundKey tuple containing the "chain of keys"
|
@@ -83,28 +94,28 @@ def dict_to_flatdict(
|
|
83
94
|
|
84
95
|
Args:
|
85
96
|
dct (dict): The dictionary to flatten
|
86
|
-
_parent (Tuple, optional): The current parent for recursion
|
87
97
|
|
88
98
|
Returns:
|
89
99
|
A flattened dict of the same type as dct
|
90
100
|
"""
|
91
|
-
typ = cast(Type[Dict[Tuple[KT, ...], Any]], type(dct))
|
92
|
-
items: List[Tuple[Tuple[KT, ...], Any]] = []
|
93
|
-
parent = _parent or tuple()
|
94
|
-
|
95
|
-
for k, v in dct.items():
|
96
|
-
k_parent = tuple(parent + (k,))
|
97
|
-
# if v is a non-empty dict, recurse
|
98
|
-
if isinstance(v, dict) and v:
|
99
|
-
items.extend(dict_to_flatdict(v, _parent=k_parent).items())
|
100
|
-
else:
|
101
|
-
items.append((k_parent, v))
|
102
|
-
return typ(items)
|
103
101
|
|
102
|
+
def flatten(
|
103
|
+
dct: NestedDict[KT, VT], _parent: tuple[KT, ...] = ()
|
104
|
+
) -> Iterator[tuple[tuple[KT, ...], VT]]:
|
105
|
+
parent = _parent or ()
|
106
|
+
for k, v in dct.items():
|
107
|
+
k_parent = (*parent, k)
|
108
|
+
# if v is a non-empty dict, recurse
|
109
|
+
if isinstance(v, dict) and v:
|
110
|
+
yield from flatten(cast(NestedDict[KT, VT], v), _parent=k_parent)
|
111
|
+
else:
|
112
|
+
yield (k_parent, cast(VT, v))
|
104
113
|
|
105
|
-
|
106
|
-
dct
|
107
|
-
|
114
|
+
type_ = cast(type[dict[tuple[KT, ...], VT]], type(dct))
|
115
|
+
return type_(flatten(dct))
|
116
|
+
|
117
|
+
|
118
|
+
def flatdict_to_dict(dct: dict[tuple[KT, ...], VT]) -> NestedDict[KT, VT]:
|
108
119
|
"""Converts a flattened dictionary back to a nested dictionary.
|
109
120
|
|
110
121
|
Args:
|
@@ -114,16 +125,26 @@ def flatdict_to_dict(
|
|
114
125
|
Returns
|
115
126
|
A nested dict of the same type as dct
|
116
127
|
"""
|
117
|
-
|
118
|
-
|
128
|
+
|
129
|
+
type_ = cast(type[NestedDict[KT, VT]], type(dct))
|
130
|
+
|
131
|
+
def new(type_: type[NestedDict[KT, VT]] = type_) -> NestedDict[KT, VT]:
|
132
|
+
return type_()
|
133
|
+
|
134
|
+
result = new()
|
119
135
|
for key_tuple, value in dct.items():
|
120
|
-
|
121
|
-
|
136
|
+
current = result
|
137
|
+
*prefix_keys, last_key = key_tuple
|
138
|
+
for prefix_key in prefix_keys:
|
122
139
|
# Build nested dictionaries up for the current key tuple
|
123
|
-
|
124
|
-
|
140
|
+
try:
|
141
|
+
current = cast(NestedDict[KT, VT], current[prefix_key])
|
142
|
+
except KeyError:
|
143
|
+
new_dict = current[prefix_key] = new()
|
144
|
+
current = new_dict
|
145
|
+
|
125
146
|
# Set the value
|
126
|
-
|
147
|
+
current[last_key] = value
|
127
148
|
|
128
149
|
return result
|
129
150
|
|
@@ -148,9 +169,9 @@ def isiterable(obj: Any) -> bool:
|
|
148
169
|
return not isinstance(obj, (str, bytes, io.IOBase))
|
149
170
|
|
150
171
|
|
151
|
-
def ensure_iterable(obj: Union[T, Iterable[T]]) ->
|
172
|
+
def ensure_iterable(obj: Union[T, Iterable[T]]) -> Collection[T]:
|
152
173
|
if isinstance(obj, Sequence) or isinstance(obj, Set):
|
153
|
-
return obj
|
174
|
+
return cast(Collection[T], obj)
|
154
175
|
obj = cast(T, obj) # No longer in the iterable case
|
155
176
|
return [obj]
|
156
177
|
|
@@ -160,9 +181,9 @@ def listrepr(objs: Iterable[Any], sep: str = " ") -> str:
|
|
160
181
|
|
161
182
|
|
162
183
|
def extract_instances(
|
163
|
-
objects: Iterable,
|
164
|
-
types: Union[
|
165
|
-
) -> Union[
|
184
|
+
objects: Iterable[Any],
|
185
|
+
types: Union[type[T], tuple[type[T], ...]] = object,
|
186
|
+
) -> Union[list[T], dict[type[T], list[T]]]:
|
166
187
|
"""
|
167
188
|
Extract objects from a file and returns a dict of type -> instances
|
168
189
|
|
@@ -174,26 +195,27 @@ def extract_instances(
|
|
174
195
|
If a single type is given: a list of instances of that type
|
175
196
|
If a tuple of types is given: a mapping of type to a list of instances
|
176
197
|
"""
|
177
|
-
|
198
|
+
types_collection = ensure_iterable(types)
|
178
199
|
|
179
200
|
# Create a mapping of type -> instance from the exec values
|
180
|
-
ret =
|
201
|
+
ret: dict[type[T], list[Any]] = {}
|
181
202
|
|
182
203
|
for o in objects:
|
183
204
|
# We iterate here so that the key is the passed type rather than type(o)
|
184
|
-
for type_ in
|
205
|
+
for type_ in types_collection:
|
185
206
|
if isinstance(o, type_):
|
186
|
-
ret[
|
207
|
+
ret.setdefault(type_, []).append(o)
|
187
208
|
|
188
|
-
if len(
|
189
|
-
|
209
|
+
if len(types_collection) == 1:
|
210
|
+
[type_] = types_collection
|
211
|
+
return ret[type_]
|
190
212
|
|
191
213
|
return ret
|
192
214
|
|
193
215
|
|
194
216
|
def batched_iterable(
|
195
217
|
iterable: Iterable[T], size: int
|
196
|
-
) -> Generator[
|
218
|
+
) -> Generator[tuple[T, ...], None, None]:
|
197
219
|
"""
|
198
220
|
Yield batches of a certain size from an iterable
|
199
221
|
|
@@ -221,15 +243,86 @@ class StopVisiting(BaseException):
|
|
221
243
|
"""
|
222
244
|
|
223
245
|
|
246
|
+
@overload
|
247
|
+
def visit_collection(
|
248
|
+
expr: Any,
|
249
|
+
visit_fn: Callable[[Any, dict[str, VT]], Any],
|
250
|
+
*,
|
251
|
+
return_data: Literal[True] = ...,
|
252
|
+
max_depth: int = ...,
|
253
|
+
context: dict[str, VT] = ...,
|
254
|
+
remove_annotations: bool = ...,
|
255
|
+
_seen: Optional[set[int]] = ...,
|
256
|
+
) -> Any:
|
257
|
+
...
|
258
|
+
|
259
|
+
|
260
|
+
@overload
|
261
|
+
def visit_collection(
|
262
|
+
expr: Any,
|
263
|
+
visit_fn: Callable[[Any], Any],
|
264
|
+
*,
|
265
|
+
return_data: Literal[True] = ...,
|
266
|
+
max_depth: int = ...,
|
267
|
+
context: None = None,
|
268
|
+
remove_annotations: bool = ...,
|
269
|
+
_seen: Optional[set[int]] = ...,
|
270
|
+
) -> Any:
|
271
|
+
...
|
272
|
+
|
273
|
+
|
274
|
+
@overload
|
275
|
+
def visit_collection(
|
276
|
+
expr: Any,
|
277
|
+
visit_fn: Callable[[Any, dict[str, VT]], Any],
|
278
|
+
*,
|
279
|
+
return_data: bool = ...,
|
280
|
+
max_depth: int = ...,
|
281
|
+
context: dict[str, VT] = ...,
|
282
|
+
remove_annotations: bool = ...,
|
283
|
+
_seen: Optional[set[int]] = ...,
|
284
|
+
) -> Optional[Any]:
|
285
|
+
...
|
286
|
+
|
287
|
+
|
288
|
+
@overload
|
289
|
+
def visit_collection(
|
290
|
+
expr: Any,
|
291
|
+
visit_fn: Callable[[Any], Any],
|
292
|
+
*,
|
293
|
+
return_data: bool = ...,
|
294
|
+
max_depth: int = ...,
|
295
|
+
context: None = None,
|
296
|
+
remove_annotations: bool = ...,
|
297
|
+
_seen: Optional[set[int]] = ...,
|
298
|
+
) -> Optional[Any]:
|
299
|
+
...
|
300
|
+
|
301
|
+
|
302
|
+
@overload
|
303
|
+
def visit_collection(
|
304
|
+
expr: Any,
|
305
|
+
visit_fn: Callable[[Any, dict[str, VT]], Any],
|
306
|
+
*,
|
307
|
+
return_data: Literal[False] = False,
|
308
|
+
max_depth: int = ...,
|
309
|
+
context: dict[str, VT] = ...,
|
310
|
+
remove_annotations: bool = ...,
|
311
|
+
_seen: Optional[set[int]] = ...,
|
312
|
+
) -> None:
|
313
|
+
...
|
314
|
+
|
315
|
+
|
224
316
|
def visit_collection(
|
225
317
|
expr: Any,
|
226
|
-
visit_fn: Union[Callable[[Any,
|
318
|
+
visit_fn: Union[Callable[[Any, dict[str, VT]], Any], Callable[[Any], Any]],
|
319
|
+
*,
|
227
320
|
return_data: bool = False,
|
228
321
|
max_depth: int = -1,
|
229
|
-
context: Optional[dict] = None,
|
322
|
+
context: Optional[dict[str, VT]] = None,
|
230
323
|
remove_annotations: bool = False,
|
231
|
-
_seen: Optional[
|
232
|
-
) -> Any:
|
324
|
+
_seen: Optional[set[int]] = None,
|
325
|
+
) -> Optional[Any]:
|
233
326
|
"""
|
234
327
|
Visits and potentially transforms every element of an arbitrary Python collection.
|
235
328
|
|
@@ -289,24 +382,39 @@ def visit_collection(
|
|
289
382
|
if _seen is None:
|
290
383
|
_seen = set()
|
291
384
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
385
|
+
if context is not None:
|
386
|
+
_callback = cast(Callable[[Any, dict[str, VT]], Any], visit_fn)
|
387
|
+
|
388
|
+
def visit_nested(expr: Any) -> Optional[Any]:
|
389
|
+
return visit_collection(
|
390
|
+
expr,
|
391
|
+
_callback,
|
392
|
+
return_data=return_data,
|
393
|
+
remove_annotations=remove_annotations,
|
394
|
+
max_depth=max_depth - 1,
|
395
|
+
# Copy the context on nested calls so it does not "propagate up"
|
396
|
+
context=context.copy(),
|
397
|
+
_seen=_seen,
|
398
|
+
)
|
399
|
+
|
400
|
+
def visit_expression(expr: Any) -> Any:
|
401
|
+
return _callback(expr, context)
|
402
|
+
else:
|
403
|
+
_callback = cast(Callable[[Any], Any], visit_fn)
|
404
|
+
|
405
|
+
def visit_nested(expr: Any) -> Optional[Any]:
|
406
|
+
# Utility for a recursive call, preserving options and updating the depth.
|
407
|
+
return visit_collection(
|
408
|
+
expr,
|
409
|
+
_callback,
|
410
|
+
return_data=return_data,
|
411
|
+
remove_annotations=remove_annotations,
|
412
|
+
max_depth=max_depth - 1,
|
413
|
+
_seen=_seen,
|
414
|
+
)
|
415
|
+
|
416
|
+
def visit_expression(expr: Any) -> Any:
|
417
|
+
return _callback(expr)
|
310
418
|
|
311
419
|
# --- 1. Visit every expression
|
312
420
|
try:
|
@@ -329,10 +437,6 @@ def visit_collection(
|
|
329
437
|
else:
|
330
438
|
_seen.add(id(expr))
|
331
439
|
|
332
|
-
# Get the expression type; treat iterators like lists
|
333
|
-
typ = list if isinstance(expr, IteratorABC) and isiterable(expr) else type(expr)
|
334
|
-
typ = cast(type, typ) # mypy treats this as 'object' otherwise and complains
|
335
|
-
|
336
440
|
# Then visit every item in the expression if it is a collection
|
337
441
|
|
338
442
|
# presume that the result is the original expression.
|
@@ -354,9 +458,10 @@ def visit_collection(
|
|
354
458
|
# --- Annotations (unmapped, quote, etc.)
|
355
459
|
|
356
460
|
elif isinstance(expr, BaseAnnotation):
|
461
|
+
annotated = cast(BaseAnnotation[Any], expr)
|
357
462
|
if context is not None:
|
358
|
-
context["annotation"] =
|
359
|
-
unwrapped =
|
463
|
+
context["annotation"] = cast(VT, annotated)
|
464
|
+
unwrapped = annotated.unwrap()
|
360
465
|
value = visit_nested(unwrapped)
|
361
466
|
|
362
467
|
if return_data:
|
@@ -365,47 +470,49 @@ def visit_collection(
|
|
365
470
|
result = value
|
366
471
|
# if the value was modified, rewrap it
|
367
472
|
elif value is not unwrapped:
|
368
|
-
result =
|
473
|
+
result = annotated.rewrap(value)
|
369
474
|
# otherwise return the expr
|
370
475
|
|
371
476
|
# --- Sequences
|
372
477
|
|
373
478
|
elif isinstance(expr, (list, tuple, set)):
|
374
|
-
|
479
|
+
seq = cast(Union[list[Any], tuple[Any], set[Any]], expr)
|
480
|
+
items = [visit_nested(o) for o in seq]
|
375
481
|
if return_data:
|
376
|
-
modified = any(item is not orig for item, orig in zip(items,
|
482
|
+
modified = any(item is not orig for item, orig in zip(items, seq))
|
377
483
|
if modified:
|
378
|
-
result =
|
484
|
+
result = type(seq)(items)
|
379
485
|
|
380
486
|
# --- Dictionaries
|
381
487
|
|
382
|
-
elif
|
383
|
-
|
384
|
-
items = [(visit_nested(k), visit_nested(v)) for k, v in
|
488
|
+
elif isinstance(expr, (dict, OrderedDict)):
|
489
|
+
mapping = cast(dict[Any, Any], expr)
|
490
|
+
items = [(visit_nested(k), visit_nested(v)) for k, v in mapping.items()]
|
385
491
|
if return_data:
|
386
492
|
modified = any(
|
387
493
|
k1 is not k2 or v1 is not v2
|
388
|
-
for (k1, v1), (k2, v2) in zip(items,
|
494
|
+
for (k1, v1), (k2, v2) in zip(items, mapping.items())
|
389
495
|
)
|
390
496
|
if modified:
|
391
|
-
result =
|
497
|
+
result = type(mapping)(items)
|
392
498
|
|
393
499
|
# --- Dataclasses
|
394
500
|
|
395
501
|
elif is_dataclass(expr) and not isinstance(expr, type):
|
396
|
-
|
502
|
+
expr_fields = fields(expr)
|
503
|
+
values = [visit_nested(getattr(expr, f.name)) for f in expr_fields]
|
397
504
|
if return_data:
|
398
505
|
modified = any(
|
399
|
-
getattr(expr, f.name) is not v for f, v in zip(
|
506
|
+
getattr(expr, f.name) is not v for f, v in zip(expr_fields, values)
|
400
507
|
)
|
401
508
|
if modified:
|
402
|
-
result =
|
509
|
+
result = replace(
|
510
|
+
expr, **{f.name: v for f, v in zip(expr_fields, values)}
|
511
|
+
)
|
403
512
|
|
404
513
|
# --- Pydantic models
|
405
514
|
|
406
515
|
elif isinstance(expr, pydantic.BaseModel):
|
407
|
-
typ = cast(Type[pydantic.BaseModel], typ)
|
408
|
-
|
409
516
|
# when extra=allow, fields not in model_fields may be in model_fields_set
|
410
517
|
model_fields = expr.model_fields_set.union(expr.model_fields.keys())
|
411
518
|
|
@@ -424,7 +531,7 @@ def visit_collection(
|
|
424
531
|
)
|
425
532
|
if modified:
|
426
533
|
# Use construct to avoid validation and handle immutability
|
427
|
-
model_instance =
|
534
|
+
model_instance = expr.model_construct(
|
428
535
|
_fields_set=expr.model_fields_set, **updated_data
|
429
536
|
)
|
430
537
|
for private_attr in expr.__private_attributes__:
|
@@ -435,7 +542,21 @@ def visit_collection(
|
|
435
542
|
return result
|
436
543
|
|
437
544
|
|
438
|
-
|
545
|
+
@overload
|
546
|
+
def remove_nested_keys(
|
547
|
+
keys_to_remove: list[HashableT], obj: NestedDict[HashableT, VT]
|
548
|
+
) -> NestedDict[HashableT, VT]:
|
549
|
+
...
|
550
|
+
|
551
|
+
|
552
|
+
@overload
|
553
|
+
def remove_nested_keys(keys_to_remove: list[HashableT], obj: Any) -> Any:
|
554
|
+
...
|
555
|
+
|
556
|
+
|
557
|
+
def remove_nested_keys(
|
558
|
+
keys_to_remove: list[HashableT], obj: Union[NestedDict[HashableT, VT], Any]
|
559
|
+
) -> Union[NestedDict[HashableT, VT], Any]:
|
439
560
|
"""
|
440
561
|
Recurses a dictionary returns a copy without all keys that match an entry in
|
441
562
|
`key_to_remove`. Return `obj` unchanged if not a dictionary.
|
@@ -452,24 +573,56 @@ def remove_nested_keys(keys_to_remove: List[Hashable], obj):
|
|
452
573
|
return obj
|
453
574
|
return {
|
454
575
|
key: remove_nested_keys(keys_to_remove, value)
|
455
|
-
for key, value in obj.items()
|
576
|
+
for key, value in cast(NestedDict[HashableT, VT], obj).items()
|
456
577
|
if key not in keys_to_remove
|
457
578
|
}
|
458
579
|
|
459
580
|
|
581
|
+
@overload
|
582
|
+
def distinct(iterable: Iterable[HashableT], key: None = None) -> Iterator[HashableT]:
|
583
|
+
...
|
584
|
+
|
585
|
+
|
586
|
+
@overload
|
587
|
+
def distinct(iterable: Iterable[T], key: Callable[[T], Hashable]) -> Iterator[T]:
|
588
|
+
...
|
589
|
+
|
590
|
+
|
460
591
|
def distinct(
|
461
|
-
iterable: Iterable[T],
|
462
|
-
key: Callable[[T],
|
463
|
-
) ->
|
464
|
-
|
592
|
+
iterable: Iterable[Union[T, HashableT]],
|
593
|
+
key: Optional[Callable[[T], Hashable]] = None,
|
594
|
+
) -> Iterator[Union[T, HashableT]]:
|
595
|
+
def _key(__i: Any) -> Hashable:
|
596
|
+
return __i
|
597
|
+
|
598
|
+
if key is not None:
|
599
|
+
_key = cast(Callable[[Any], Hashable], key)
|
600
|
+
|
601
|
+
seen: set[Hashable] = set()
|
465
602
|
for item in iterable:
|
466
|
-
if
|
603
|
+
if _key(item) in seen:
|
467
604
|
continue
|
468
|
-
seen.add(
|
605
|
+
seen.add(_key(item))
|
469
606
|
yield item
|
470
607
|
|
471
608
|
|
472
|
-
|
609
|
+
@overload
|
610
|
+
def get_from_dict(
|
611
|
+
dct: NestedDict[str, VT], keys: Union[str, list[str]], default: None = None
|
612
|
+
) -> Optional[VT]:
|
613
|
+
...
|
614
|
+
|
615
|
+
|
616
|
+
@overload
|
617
|
+
def get_from_dict(
|
618
|
+
dct: NestedDict[str, VT], keys: Union[str, list[str]], default: R
|
619
|
+
) -> Union[VT, R]:
|
620
|
+
...
|
621
|
+
|
622
|
+
|
623
|
+
def get_from_dict(
|
624
|
+
dct: NestedDict[str, VT], keys: Union[str, list[str]], default: Optional[R] = None
|
625
|
+
) -> Union[VT, R, None]:
|
473
626
|
"""
|
474
627
|
Fetch a value from a nested dictionary or list using a sequence of keys.
|
475
628
|
|
@@ -500,6 +653,7 @@ def get_from_dict(dct: Dict, keys: Union[str, List[str]], default: Any = None) -
|
|
500
653
|
"""
|
501
654
|
if isinstance(keys, str):
|
502
655
|
keys = keys.replace("[", ".").replace("]", "").split(".")
|
656
|
+
value = dct
|
503
657
|
try:
|
504
658
|
for key in keys:
|
505
659
|
try:
|
@@ -509,13 +663,15 @@ def get_from_dict(dct: Dict, keys: Union[str, List[str]], default: Any = None) -
|
|
509
663
|
# If it's not an int, use the key as-is
|
510
664
|
# for dict lookup
|
511
665
|
pass
|
512
|
-
|
513
|
-
return
|
666
|
+
value = value[key] # type: ignore
|
667
|
+
return cast(VT, value)
|
514
668
|
except (TypeError, KeyError, IndexError):
|
515
669
|
return default
|
516
670
|
|
517
671
|
|
518
|
-
def set_in_dict(
|
672
|
+
def set_in_dict(
|
673
|
+
dct: NestedDict[str, VT], keys: Union[str, list[str]], value: VT
|
674
|
+
) -> None:
|
519
675
|
"""
|
520
676
|
Sets a value in a nested dictionary using a sequence of keys.
|
521
677
|
|
@@ -543,11 +699,13 @@ def set_in_dict(dct: Dict, keys: Union[str, List[str]], value: Any):
|
|
543
699
|
raise TypeError(f"Key path exists and contains a non-dict value: {keys}")
|
544
700
|
if k not in dct:
|
545
701
|
dct[k] = {}
|
546
|
-
dct = dct[k]
|
702
|
+
dct = cast(NestedDict[str, VT], dct[k])
|
547
703
|
dct[keys[-1]] = value
|
548
704
|
|
549
705
|
|
550
|
-
def deep_merge(
|
706
|
+
def deep_merge(
|
707
|
+
dct: NestedDict[str, VT1], merge: NestedDict[str, VT2]
|
708
|
+
) -> NestedDict[str, Union[VT1, VT2]]:
|
551
709
|
"""
|
552
710
|
Recursively merges `merge` into `dct`.
|
553
711
|
|
@@ -558,18 +716,21 @@ def deep_merge(dct: Dict, merge: Dict):
|
|
558
716
|
Returns:
|
559
717
|
A new dictionary with the merged contents.
|
560
718
|
"""
|
561
|
-
result = dct.copy() # Start with keys and values from `dct`
|
719
|
+
result: dict[str, Any] = dct.copy() # Start with keys and values from `dct`
|
562
720
|
for key, value in merge.items():
|
563
721
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
564
722
|
# If both values are dictionaries, merge them recursively
|
565
|
-
result[key] = deep_merge(
|
723
|
+
result[key] = deep_merge(
|
724
|
+
cast(NestedDict[str, VT1], result[key]),
|
725
|
+
cast(NestedDict[str, VT2], value),
|
726
|
+
)
|
566
727
|
else:
|
567
728
|
# Otherwise, overwrite with the new value
|
568
|
-
result[key] = value
|
729
|
+
result[key] = cast(Union[VT2, NestedDict[str, VT2]], value)
|
569
730
|
return result
|
570
731
|
|
571
732
|
|
572
|
-
def deep_merge_dicts(*dicts):
|
733
|
+
def deep_merge_dicts(*dicts: NestedDict[str, Any]) -> NestedDict[str, Any]:
|
573
734
|
"""
|
574
735
|
Recursively merges multiple dictionaries.
|
575
736
|
|
@@ -579,7 +740,7 @@ def deep_merge_dicts(*dicts):
|
|
579
740
|
Returns:
|
580
741
|
A new dictionary with the merged contents.
|
581
742
|
"""
|
582
|
-
result = {}
|
743
|
+
result: NestedDict[str, Any] = {}
|
583
744
|
for dictionary in dicts:
|
584
745
|
result = deep_merge(result, dictionary)
|
585
746
|
return result
|
prefect/utilities/compat.py
CHANGED
@@ -3,29 +3,21 @@ Utilities for Python version compatibility
|
|
3
3
|
"""
|
4
4
|
# Please organize additions to this file by version
|
5
5
|
|
6
|
-
import asyncio
|
7
6
|
import sys
|
8
|
-
from shutil import copytree
|
9
|
-
from signal import raise_signal
|
10
7
|
|
11
8
|
if sys.version_info < (3, 10):
|
12
|
-
import importlib_metadata
|
13
|
-
from importlib_metadata import
|
9
|
+
import importlib_metadata as importlib_metadata
|
10
|
+
from importlib_metadata import (
|
11
|
+
EntryPoint as EntryPoint,
|
12
|
+
EntryPoints as EntryPoints,
|
13
|
+
entry_points as entry_points,
|
14
|
+
)
|
14
15
|
else:
|
15
|
-
import importlib.metadata
|
16
|
-
from importlib.metadata import
|
16
|
+
import importlib.metadata
|
17
|
+
from importlib.metadata import (
|
18
|
+
EntryPoint as EntryPoint,
|
19
|
+
EntryPoints as EntryPoints,
|
20
|
+
entry_points as entry_points,
|
21
|
+
)
|
17
22
|
|
18
|
-
|
19
|
-
# https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
|
20
|
-
|
21
|
-
import functools
|
22
|
-
|
23
|
-
async def asyncio_to_thread(fn, *args, **kwargs):
|
24
|
-
loop = asyncio.get_running_loop()
|
25
|
-
return await loop.run_in_executor(None, functools.partial(fn, *args, **kwargs))
|
26
|
-
|
27
|
-
else:
|
28
|
-
from asyncio import to_thread as asyncio_to_thread
|
29
|
-
|
30
|
-
if sys.platform != "win32":
|
31
|
-
from asyncio import ThreadedChildWatcher
|
23
|
+
importlib_metadata = importlib.metadata
|