prefect-client 3.1.4__py3-none-any.whl → 3.1.6__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 (96) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_internal/compatibility/migration.py +1 -1
  3. prefect/_internal/concurrency/api.py +52 -52
  4. prefect/_internal/concurrency/calls.py +59 -35
  5. prefect/_internal/concurrency/cancellation.py +34 -18
  6. prefect/_internal/concurrency/event_loop.py +7 -6
  7. prefect/_internal/concurrency/threads.py +41 -33
  8. prefect/_internal/concurrency/waiters.py +28 -21
  9. prefect/_internal/pydantic/v1_schema.py +2 -2
  10. prefect/_internal/pydantic/v2_schema.py +10 -9
  11. prefect/_internal/schemas/bases.py +10 -11
  12. prefect/_internal/schemas/validators.py +2 -1
  13. prefect/_version.py +3 -3
  14. prefect/automations.py +53 -47
  15. prefect/blocks/abstract.py +12 -10
  16. prefect/blocks/core.py +4 -2
  17. prefect/cache_policies.py +11 -11
  18. prefect/client/__init__.py +3 -1
  19. prefect/client/base.py +36 -37
  20. prefect/client/cloud.py +26 -19
  21. prefect/client/collections.py +2 -2
  22. prefect/client/orchestration.py +366 -277
  23. prefect/client/schemas/__init__.py +24 -0
  24. prefect/client/schemas/actions.py +132 -120
  25. prefect/client/schemas/filters.py +5 -0
  26. prefect/client/schemas/objects.py +113 -85
  27. prefect/client/schemas/responses.py +21 -18
  28. prefect/client/schemas/schedules.py +136 -93
  29. prefect/client/subscriptions.py +28 -14
  30. prefect/client/utilities.py +32 -36
  31. prefect/concurrency/asyncio.py +6 -9
  32. prefect/concurrency/services.py +3 -0
  33. prefect/concurrency/sync.py +35 -5
  34. prefect/context.py +39 -31
  35. prefect/deployments/flow_runs.py +3 -5
  36. prefect/docker/__init__.py +1 -1
  37. prefect/events/schemas/events.py +25 -20
  38. prefect/events/utilities.py +1 -2
  39. prefect/filesystems.py +3 -3
  40. prefect/flow_engine.py +755 -138
  41. prefect/flow_runs.py +3 -3
  42. prefect/flows.py +214 -170
  43. prefect/logging/configuration.py +1 -1
  44. prefect/logging/highlighters.py +1 -2
  45. prefect/logging/loggers.py +30 -20
  46. prefect/main.py +17 -24
  47. prefect/runner/runner.py +43 -21
  48. prefect/runner/server.py +30 -32
  49. prefect/runner/submit.py +3 -6
  50. prefect/runner/utils.py +6 -6
  51. prefect/runtime/flow_run.py +7 -0
  52. prefect/settings/constants.py +2 -2
  53. prefect/settings/legacy.py +1 -1
  54. prefect/settings/models/server/events.py +10 -0
  55. prefect/settings/sources.py +9 -2
  56. prefect/task_engine.py +72 -19
  57. prefect/task_runners.py +2 -2
  58. prefect/tasks.py +46 -33
  59. prefect/telemetry/bootstrap.py +15 -2
  60. prefect/telemetry/run_telemetry.py +107 -0
  61. prefect/transactions.py +14 -14
  62. prefect/types/__init__.py +20 -3
  63. prefect/utilities/_engine.py +96 -0
  64. prefect/utilities/annotations.py +25 -18
  65. prefect/utilities/asyncutils.py +126 -140
  66. prefect/utilities/callables.py +87 -78
  67. prefect/utilities/collections.py +278 -117
  68. prefect/utilities/compat.py +13 -21
  69. prefect/utilities/context.py +6 -5
  70. prefect/utilities/dispatch.py +23 -12
  71. prefect/utilities/dockerutils.py +33 -32
  72. prefect/utilities/engine.py +126 -239
  73. prefect/utilities/filesystem.py +18 -15
  74. prefect/utilities/hashing.py +10 -11
  75. prefect/utilities/importtools.py +40 -27
  76. prefect/utilities/math.py +9 -5
  77. prefect/utilities/names.py +3 -3
  78. prefect/utilities/processutils.py +121 -57
  79. prefect/utilities/pydantic.py +41 -36
  80. prefect/utilities/render_swagger.py +22 -12
  81. prefect/utilities/schema_tools/__init__.py +2 -1
  82. prefect/utilities/schema_tools/hydration.py +50 -43
  83. prefect/utilities/schema_tools/validation.py +52 -42
  84. prefect/utilities/services.py +13 -12
  85. prefect/utilities/templating.py +45 -45
  86. prefect/utilities/text.py +2 -1
  87. prefect/utilities/timeout.py +4 -4
  88. prefect/utilities/urls.py +9 -4
  89. prefect/utilities/visualization.py +46 -24
  90. prefect/variables.py +9 -8
  91. prefect/workers/base.py +18 -10
  92. {prefect_client-3.1.4.dist-info → prefect_client-3.1.6.dist-info}/METADATA +5 -5
  93. {prefect_client-3.1.4.dist-info → prefect_client-3.1.6.dist-info}/RECORD +96 -94
  94. {prefect_client-3.1.4.dist-info → prefect_client-3.1.6.dist-info}/WHEEL +1 -1
  95. {prefect_client-3.1.4.dist-info → prefect_client-3.1.6.dist-info}/LICENSE +0 -0
  96. {prefect_client-3.1.4.dist-info → prefect_client-3.1.6.dist-info}/top_level.txt +0 -0
@@ -6,33 +6,40 @@ import io
6
6
  import itertools
7
7
  import types
8
8
  import warnings
9
- from collections import OrderedDict, defaultdict
10
- from collections.abc import Iterator as IteratorABC
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
- Dict,
12
+ Collection,
18
13
  Generator,
19
14
  Hashable,
20
15
  Iterable,
21
- List,
22
- Optional,
16
+ Iterator,
17
+ Sequence,
23
18
  Set,
24
- Tuple,
25
- Type,
26
- TypeVar,
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, Quote, quote # noqa
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
- def _generate_next_value_(name, start, count, last_values):
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
- def flatdict_to_dict(
106
- dct: Dict[Tuple[KT, ...], VT],
107
- ) -> Dict[KT, Union[VT, Dict[KT, VT]]]:
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
- typ = type(dct)
118
- result = cast(Dict[KT, Union[VT, Dict[KT, VT]]], typ())
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
- current_dict = result
121
- for prefix_key in key_tuple[:-1]:
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
- # Use `setdefault` in case the nested dict has already been created
124
- current_dict = current_dict.setdefault(prefix_key, typ()) # type: ignore
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
- current_dict[key_tuple[-1]] = value
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]]) -> 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[Type[T], Tuple[Type[T], ...]] = object,
165
- ) -> Union[List[T], Dict[Type[T], T]]:
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
- types = ensure_iterable(types)
198
+ types_collection = ensure_iterable(types)
178
199
 
179
200
  # Create a mapping of type -> instance from the exec values
180
- ret = defaultdict(list)
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 types:
205
+ for type_ in types_collection:
185
206
  if isinstance(o, type_):
186
- ret[type_].append(o)
207
+ ret.setdefault(type_, []).append(o)
187
208
 
188
- if len(types) == 1:
189
- return ret[types[0]]
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[Tuple[T, ...], None, None]:
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, Optional[dict]], Any], Callable[[Any], 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[Set[int]] = None,
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
- def visit_nested(expr):
293
- # Utility for a recursive call, preserving options and updating the depth.
294
- return visit_collection(
295
- expr,
296
- visit_fn=visit_fn,
297
- return_data=return_data,
298
- remove_annotations=remove_annotations,
299
- max_depth=max_depth - 1,
300
- # Copy the context on nested calls so it does not "propagate up"
301
- context=context.copy() if context is not None else None,
302
- _seen=_seen,
303
- )
304
-
305
- def visit_expression(expr):
306
- if context is not None:
307
- return visit_fn(expr, context)
308
- else:
309
- return visit_fn(expr)
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"] = expr
359
- unwrapped = expr.unwrap()
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 = expr.rewrap(value)
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
- items = [visit_nested(o) for o in expr]
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, expr))
482
+ modified = any(item is not orig for item, orig in zip(items, seq))
377
483
  if modified:
378
- result = typ(items)
484
+ result = type(seq)(items)
379
485
 
380
486
  # --- Dictionaries
381
487
 
382
- elif typ in (dict, OrderedDict):
383
- assert isinstance(expr, (dict, OrderedDict)) # typecheck assertion
384
- items = [(visit_nested(k), visit_nested(v)) for k, v in expr.items()]
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, expr.items())
494
+ for (k1, v1), (k2, v2) in zip(items, mapping.items())
389
495
  )
390
496
  if modified:
391
- result = typ(items)
497
+ result = type(mapping)(items)
392
498
 
393
499
  # --- Dataclasses
394
500
 
395
501
  elif is_dataclass(expr) and not isinstance(expr, type):
396
- values = [visit_nested(getattr(expr, f.name)) for f in fields(expr)]
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(fields(expr), values)
506
+ getattr(expr, f.name) is not v for f, v in zip(expr_fields, values)
400
507
  )
401
508
  if modified:
402
- result = typ(**{f.name: v for f, v in zip(fields(expr), values)})
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 = typ.model_construct(
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
- def remove_nested_keys(keys_to_remove: List[Hashable], obj):
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], Any] = (lambda i: i),
463
- ) -> Generator[T, None, None]:
464
- seen: Set = set()
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 key(item) in seen:
603
+ if _key(item) in seen:
467
604
  continue
468
- seen.add(key(item))
605
+ seen.add(_key(item))
469
606
  yield item
470
607
 
471
608
 
472
- def get_from_dict(dct: Dict, keys: Union[str, List[str]], default: Any = None) -> Any:
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
- dct = dct[key]
513
- return dct
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(dct: Dict, keys: Union[str, List[str]], value: Any):
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(dct: Dict, merge: Dict):
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(result[key], value)
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
@@ -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 EntryPoint, EntryPoints, entry_points
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 as importlib_metadata
16
- from importlib.metadata import EntryPoint, EntryPoints, entry_points
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
- if sys.version_info < (3, 9):
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