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
@@ -1,10 +1,12 @@
1
1
  import json
2
- from typing import Any, Callable, Dict, Optional
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any, Optional, cast
3
5
 
4
6
  import jinja2
5
7
  from pydantic import BaseModel, Field
6
8
  from sqlalchemy.ext.asyncio import AsyncSession
7
- from typing_extensions import TypeAlias
9
+ from typing_extensions import Self, TypeAlias, TypeIs
8
10
 
9
11
  from prefect.server.utilities.user_templates import (
10
12
  TemplateSecurityError,
@@ -15,14 +17,14 @@ from prefect.types import StrictVariableValue
15
17
 
16
18
 
17
19
  class HydrationContext(BaseModel):
18
- workspace_variables: Dict[
20
+ workspace_variables: dict[
19
21
  str,
20
22
  StrictVariableValue,
21
23
  ] = Field(default_factory=dict)
22
24
  render_workspace_variables: bool = Field(default=False)
23
25
  raise_on_error: bool = Field(default=False)
24
26
  render_jinja: bool = Field(default=False)
25
- jinja_context: Dict[str, Any] = Field(default_factory=dict)
27
+ jinja_context: dict[str, Any] = Field(default_factory=dict)
26
28
 
27
29
  @classmethod
28
30
  async def build(
@@ -31,9 +33,11 @@ class HydrationContext(BaseModel):
31
33
  raise_on_error: bool = False,
32
34
  render_jinja: bool = False,
33
35
  render_workspace_variables: bool = False,
34
- ) -> "HydrationContext":
36
+ ) -> Self:
37
+ from prefect.server.database.orm_models import Variable
35
38
  from prefect.server.models.variables import read_variables
36
39
 
40
+ variables: Sequence[Variable]
37
41
  if render_workspace_variables:
38
42
  variables = await read_variables(
39
43
  session=session,
@@ -51,14 +55,14 @@ class HydrationContext(BaseModel):
51
55
  )
52
56
 
53
57
 
54
- Handler: TypeAlias = Callable[[dict, HydrationContext], Any]
58
+ Handler: TypeAlias = Callable[[dict[str, Any], HydrationContext], Any]
55
59
  PrefectKind: TypeAlias = Optional[str]
56
60
 
57
- _handlers: Dict[PrefectKind, Handler] = {}
61
+ _handlers: dict[PrefectKind, Handler] = {}
58
62
 
59
63
 
60
64
  class Placeholder:
61
- def __eq__(self, other):
65
+ def __eq__(self, other: Any) -> bool:
62
66
  return isinstance(other, type(self))
63
67
 
64
68
  @property
@@ -70,11 +74,11 @@ class RemoveValue(Placeholder):
70
74
  pass
71
75
 
72
76
 
73
- def _remove_value(value) -> bool:
77
+ def _remove_value(value: Any) -> TypeIs[RemoveValue]:
74
78
  return isinstance(value, RemoveValue)
75
79
 
76
80
 
77
- class HydrationError(Placeholder, Exception):
81
+ class HydrationError(Placeholder, Exception, ABC):
78
82
  def __init__(self, detail: Optional[str] = None):
79
83
  self.detail = detail
80
84
 
@@ -83,47 +87,49 @@ class HydrationError(Placeholder, Exception):
83
87
  return True
84
88
 
85
89
  @property
86
- def message(self):
90
+ @abstractmethod
91
+ def message(self) -> str:
87
92
  raise NotImplementedError("Must be implemented by subclass")
88
93
 
89
- def __eq__(self, other):
94
+ def __eq__(self, other: Any) -> bool:
90
95
  return isinstance(other, type(self)) and self.message == other.message
91
96
 
92
- def __str__(self):
97
+ def __str__(self) -> str:
93
98
  return self.message
94
99
 
95
100
 
96
101
  class KeyNotFound(HydrationError):
97
102
  @property
98
- def message(self):
103
+ def message(self) -> str:
99
104
  return f"Missing '{self.key}' key in __prefect object"
100
105
 
101
106
  @property
107
+ @abstractmethod
102
108
  def key(self) -> str:
103
109
  raise NotImplementedError("Must be implemented by subclass")
104
110
 
105
111
 
106
112
  class ValueNotFound(KeyNotFound):
107
113
  @property
108
- def key(self):
114
+ def key(self) -> str:
109
115
  return "value"
110
116
 
111
117
 
112
118
  class TemplateNotFound(KeyNotFound):
113
119
  @property
114
- def key(self):
120
+ def key(self) -> str:
115
121
  return "template"
116
122
 
117
123
 
118
124
  class VariableNameNotFound(KeyNotFound):
119
125
  @property
120
- def key(self):
126
+ def key(self) -> str:
121
127
  return "variable_name"
122
128
 
123
129
 
124
130
  class InvalidJSON(HydrationError):
125
131
  @property
126
- def message(self):
132
+ def message(self) -> str:
127
133
  message = "Invalid JSON"
128
134
  if self.detail:
129
135
  message += f": {self.detail}"
@@ -132,7 +138,7 @@ class InvalidJSON(HydrationError):
132
138
 
133
139
  class InvalidJinja(HydrationError):
134
140
  @property
135
- def message(self):
141
+ def message(self) -> str:
136
142
  message = "Invalid jinja"
137
143
  if self.detail:
138
144
  message += f": {self.detail}"
@@ -146,29 +152,29 @@ class WorkspaceVariableNotFound(HydrationError):
146
152
  return self.detail
147
153
 
148
154
  @property
149
- def message(self):
155
+ def message(self) -> str:
150
156
  return f"Variable '{self.detail}' not found in workspace."
151
157
 
152
158
 
153
159
  class WorkspaceVariable(Placeholder):
154
- def __init__(self, variable_name: str):
160
+ def __init__(self, variable_name: str) -> None:
155
161
  self.variable_name = variable_name
156
162
 
157
- def __eq__(self, other):
163
+ def __eq__(self, other: Any) -> bool:
158
164
  return (
159
165
  isinstance(other, type(self)) and self.variable_name == other.variable_name
160
166
  )
161
167
 
162
168
 
163
169
  class ValidJinja(Placeholder):
164
- def __init__(self, template: str):
170
+ def __init__(self, template: str) -> None:
165
171
  self.template = template
166
172
 
167
- def __eq__(self, other):
173
+ def __eq__(self, other: Any) -> bool:
168
174
  return isinstance(other, type(self)) and self.template == other.template
169
175
 
170
176
 
171
- def handler(kind: PrefectKind) -> Callable:
177
+ def handler(kind: PrefectKind) -> Callable[[Handler], Handler]:
172
178
  def decorator(func: Handler) -> Handler:
173
179
  _handlers[kind] = func
174
180
  return func
@@ -176,9 +182,9 @@ def handler(kind: PrefectKind) -> Callable:
176
182
  return decorator
177
183
 
178
184
 
179
- def call_handler(kind: PrefectKind, obj: dict, ctx: HydrationContext) -> Any:
185
+ def call_handler(kind: PrefectKind, obj: dict[str, Any], ctx: HydrationContext) -> Any:
180
186
  if kind not in _handlers:
181
- return (obj or {}).get("value", None)
187
+ return obj.get("value", None)
182
188
 
183
189
  res = _handlers[kind](obj, ctx)
184
190
  if ctx.raise_on_error and isinstance(res, HydrationError):
@@ -187,7 +193,7 @@ def call_handler(kind: PrefectKind, obj: dict, ctx: HydrationContext) -> Any:
187
193
 
188
194
 
189
195
  @handler("none")
190
- def null_handler(obj: dict, ctx: HydrationContext):
196
+ def null_handler(obj: dict[str, Any], ctx: HydrationContext):
191
197
  if "value" in obj:
192
198
  # null handler is a pass through, so we want to continue to hydrate
193
199
  return _hydrate(obj["value"], ctx)
@@ -196,7 +202,7 @@ def null_handler(obj: dict, ctx: HydrationContext):
196
202
 
197
203
 
198
204
  @handler("json")
199
- def json_handler(obj: dict, ctx: HydrationContext):
205
+ def json_handler(obj: dict[str, Any], ctx: HydrationContext):
200
206
  if "value" in obj:
201
207
  if isinstance(obj["value"], dict):
202
208
  dehydrated_json = _hydrate(obj["value"], ctx)
@@ -222,7 +228,7 @@ def json_handler(obj: dict, ctx: HydrationContext):
222
228
 
223
229
 
224
230
  @handler("jinja")
225
- def jinja_handler(obj: dict, ctx: HydrationContext):
231
+ def jinja_handler(obj: dict[str, Any], ctx: HydrationContext) -> Any:
226
232
  if "template" in obj:
227
233
  if isinstance(obj["template"], dict):
228
234
  dehydrated_jinja = _hydrate(obj["template"], ctx)
@@ -247,7 +253,7 @@ def jinja_handler(obj: dict, ctx: HydrationContext):
247
253
 
248
254
 
249
255
  @handler("workspace_variable")
250
- def workspace_variable_handler(obj: dict, ctx: HydrationContext):
256
+ def workspace_variable_handler(obj: dict[str, Any], ctx: HydrationContext) -> Any:
251
257
  if "variable_name" in obj:
252
258
  if isinstance(obj["variable_name"], dict):
253
259
  dehydrated_variable = _hydrate(obj["variable_name"], ctx)
@@ -259,7 +265,7 @@ def workspace_variable_handler(obj: dict, ctx: HydrationContext):
259
265
  return dehydrated_variable
260
266
 
261
267
  if not ctx.render_workspace_variables:
262
- return WorkspaceVariable(variable_name=obj["variable_name"])
268
+ return WorkspaceVariable(variable_name=dehydrated_variable)
263
269
 
264
270
  if dehydrated_variable in ctx.workspace_variables:
265
271
  return ctx.workspace_variables[dehydrated_variable]
@@ -277,35 +283,36 @@ def workspace_variable_handler(obj: dict, ctx: HydrationContext):
277
283
  return RemoveValue()
278
284
 
279
285
 
280
- def hydrate(obj: dict, ctx: Optional[HydrationContext] = None):
281
- res = _hydrate(obj, ctx)
286
+ def hydrate(
287
+ obj: dict[str, Any], ctx: Optional[HydrationContext] = None
288
+ ) -> dict[str, Any]:
289
+ res: dict[str, Any] = _hydrate(obj, ctx)
282
290
 
283
291
  if _remove_value(res):
284
- return {}
292
+ res = {}
285
293
 
286
294
  return res
287
295
 
288
296
 
289
- def _hydrate(obj, ctx: Optional[HydrationContext] = None) -> Any:
297
+ def _hydrate(obj: Any, ctx: Optional[HydrationContext] = None) -> Any:
290
298
  if ctx is None:
291
299
  ctx = HydrationContext()
292
300
 
293
- prefect_object = isinstance(obj, dict) and "__prefect_kind" in obj
294
-
295
- if prefect_object:
296
- prefect_kind = obj.get("__prefect_kind")
297
- return call_handler(prefect_kind, obj, ctx)
301
+ if isinstance(obj, dict) and "__prefect_kind" in obj:
302
+ obj_dict: dict[str, Any] = obj
303
+ prefect_kind = obj_dict["__prefect_kind"]
304
+ return call_handler(prefect_kind, obj_dict, ctx)
298
305
  else:
299
306
  if isinstance(obj, dict):
300
307
  return {
301
308
  key: hydrated_value
302
- for key, value in obj.items()
309
+ for key, value in cast(dict[str, Any], obj).items()
303
310
  if not _remove_value(hydrated_value := _hydrate(value, ctx))
304
311
  }
305
312
  elif isinstance(obj, list):
306
313
  return [
307
314
  hydrated_element
308
- for element in obj
315
+ for element in cast(list[Any], obj)
309
316
  if not _remove_value(hydrated_element := _hydrate(element, ctx))
310
317
  ]
311
318
  else:
@@ -1,14 +1,19 @@
1
1
  from collections import defaultdict, deque
2
+ from collections.abc import Callable, Iterable, Iterator
2
3
  from copy import deepcopy
3
- from typing import Any, Dict, List
4
+ from typing import TYPE_CHECKING, Any, cast
4
5
 
5
6
  import jsonschema
6
7
  from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
7
8
  from jsonschema.validators import Draft202012Validator, create
9
+ from referencing.jsonschema import ObjectSchema, Schema
8
10
 
9
11
  from prefect.utilities.collections import remove_nested_keys
10
12
  from prefect.utilities.schema_tools.hydration import HydrationError, Placeholder
11
13
 
14
+ if TYPE_CHECKING:
15
+ from jsonschema.validators import _Validator # type: ignore
16
+
12
17
 
13
18
  class CircularSchemaRefError(Exception):
14
19
  pass
@@ -21,12 +26,16 @@ class ValidationError(Exception):
21
26
  PLACEHOLDERS_VALIDATOR_NAME = "_placeholders"
22
27
 
23
28
 
24
- def _build_validator():
25
- def _applicable_validators(schema):
29
+ def _build_validator() -> type["_Validator"]:
30
+ def _applicable_validators(schema: Schema) -> Iterable[tuple[str, Any]]:
26
31
  # the default implementation returns `schema.items()`
27
- return {**schema, PLACEHOLDERS_VALIDATOR_NAME: None}.items()
32
+ assert not isinstance(schema, bool)
33
+ schema = {**schema, PLACEHOLDERS_VALIDATOR_NAME: None}
34
+ return schema.items()
28
35
 
29
- def _placeholders(validator, _, instance, schema):
36
+ def _placeholders(
37
+ _validator: "_Validator", _property: object, instance: Any, _schema: Schema
38
+ ) -> Iterator[JSONSchemaValidationError]:
30
39
  if isinstance(instance, HydrationError):
31
40
  yield JSONSchemaValidationError(instance.message)
32
41
 
@@ -43,7 +52,9 @@ def _build_validator():
43
52
  version="prefect",
44
53
  type_checker=Draft202012Validator.TYPE_CHECKER,
45
54
  format_checker=Draft202012Validator.FORMAT_CHECKER,
46
- id_of=Draft202012Validator.ID_OF,
55
+ id_of=cast( # the stub for create() is wrong here; id_of accepts (Schema) -> str | None
56
+ Callable[[Schema], str], Draft202012Validator.ID_OF
57
+ ),
47
58
  applicable_validators=_applicable_validators,
48
59
  )
49
60
 
@@ -51,24 +62,23 @@ def _build_validator():
51
62
  _VALIDATOR = _build_validator()
52
63
 
53
64
 
54
- def is_valid_schema(schema: Dict, preprocess: bool = True):
65
+ def is_valid_schema(schema: ObjectSchema, preprocess: bool = True) -> None:
55
66
  if preprocess:
56
67
  schema = preprocess_schema(schema)
57
68
  try:
58
- if schema is not None:
59
- _VALIDATOR.check_schema(schema, format_checker=_VALIDATOR.FORMAT_CHECKER)
69
+ _VALIDATOR.check_schema(schema, format_checker=_VALIDATOR.FORMAT_CHECKER)
60
70
  except jsonschema.SchemaError as exc:
61
71
  raise ValueError(f"Invalid schema: {exc.message}") from exc
62
72
 
63
73
 
64
74
  def validate(
65
- obj: Dict,
66
- schema: Dict,
75
+ obj: dict[str, Any],
76
+ schema: ObjectSchema,
67
77
  raise_on_error: bool = False,
68
78
  preprocess: bool = True,
69
79
  ignore_required: bool = False,
70
80
  allow_none_with_default: bool = False,
71
- ) -> List[JSONSchemaValidationError]:
81
+ ) -> list[JSONSchemaValidationError]:
72
82
  if preprocess:
73
83
  schema = preprocess_schema(schema, allow_none_with_default)
74
84
 
@@ -93,32 +103,31 @@ def validate(
93
103
  else:
94
104
  try:
95
105
  validator = _VALIDATOR(schema, format_checker=_VALIDATOR.FORMAT_CHECKER)
96
- errors = list(validator.iter_errors(obj))
106
+ errors = list(validator.iter_errors(obj)) # type: ignore
97
107
  except RecursionError:
98
108
  raise CircularSchemaRefError
99
109
  return errors
100
110
 
101
111
 
102
- def is_valid(
103
- obj: Dict,
104
- schema: Dict,
105
- ) -> bool:
112
+ def is_valid(obj: dict[str, Any], schema: ObjectSchema) -> bool:
106
113
  errors = validate(obj, schema)
107
- return len(errors) == 0
114
+ return not errors
108
115
 
109
116
 
110
- def prioritize_placeholder_errors(errors):
111
- errors_by_path = defaultdict(list)
117
+ def prioritize_placeholder_errors(
118
+ errors: list[JSONSchemaValidationError],
119
+ ) -> list[JSONSchemaValidationError]:
120
+ errors_by_path: dict[str, list[JSONSchemaValidationError]] = defaultdict(list)
112
121
  for error in errors:
113
122
  path_str = "->".join(str(p) for p in error.relative_path)
114
123
  errors_by_path[path_str].append(error)
115
124
 
116
- filtered_errors = []
117
- for path, grouped_errors in errors_by_path.items():
125
+ filtered_errors: list[JSONSchemaValidationError] = []
126
+ for grouped_errors in errors_by_path.values():
118
127
  placeholders_errors = [
119
128
  error
120
129
  for error in grouped_errors
121
- if error.validator == PLACEHOLDERS_VALIDATOR_NAME
130
+ if error.validator == PLACEHOLDERS_VALIDATOR_NAME # type: ignore # typing stubs are incomplete
122
131
  ]
123
132
 
124
133
  if placeholders_errors:
@@ -129,8 +138,8 @@ def prioritize_placeholder_errors(errors):
129
138
  return filtered_errors
130
139
 
131
140
 
132
- def build_error_obj(errors: List[JSONSchemaValidationError]) -> Dict:
133
- error_response: Dict[str, Any] = {"errors": []}
141
+ def build_error_obj(errors: list[JSONSchemaValidationError]) -> dict[str, Any]:
142
+ error_response: dict[str, Any] = {"errors": []}
134
143
 
135
144
  # If multiple errors are present for the same path and one of them
136
145
  # is a placeholder error, we want only want to use the placeholder error.
@@ -145,11 +154,11 @@ def build_error_obj(errors: List[JSONSchemaValidationError]) -> Dict:
145
154
 
146
155
  # Required errors should be moved one level down to the property
147
156
  # they're associated with, so we add an extra level to the path.
148
- if error.validator == "required":
149
- required_field = error.message.split(" ")[0].strip("'")
157
+ if error.validator == "required": # type: ignore
158
+ required_field = error.message.partition(" ")[0].strip("'")
150
159
  path.append(required_field)
151
160
 
152
- current = error_response["errors"]
161
+ current: list[Any] = error_response["errors"]
153
162
 
154
163
  # error at the root, just append the error message
155
164
  if not path:
@@ -163,10 +172,10 @@ def build_error_obj(errors: List[JSONSchemaValidationError]) -> Dict:
163
172
  else:
164
173
  for entry in current:
165
174
  if entry.get("index") == part:
166
- current = entry["errors"]
175
+ current = cast(list[Any], entry["errors"])
167
176
  break
168
177
  else:
169
- new_entry = {"index": part, "errors": []}
178
+ new_entry: dict[str, Any] = {"index": part, "errors": []}
170
179
  current.append(new_entry)
171
180
  current = new_entry["errors"]
172
181
  else:
@@ -182,7 +191,7 @@ def build_error_obj(errors: List[JSONSchemaValidationError]) -> Dict:
182
191
  current.append(new_entry)
183
192
  current = new_entry["errors"]
184
193
 
185
- valid = len(error_response["errors"]) == 0
194
+ valid = not bool(error_response["errors"])
186
195
  error_response["valid"] = valid
187
196
 
188
197
  return error_response
@@ -190,10 +199,10 @@ def build_error_obj(errors: List[JSONSchemaValidationError]) -> Dict:
190
199
 
191
200
  def _fix_null_typing(
192
201
  key: str,
193
- schema: Dict,
194
- required_fields: List[str],
202
+ schema: dict[str, Any],
203
+ required_fields: list[str],
195
204
  allow_none_with_default: bool = False,
196
- ):
205
+ ) -> None:
197
206
  """
198
207
  Pydantic V1 does not generate a valid Draft2020-12 schema for null types.
199
208
  """
@@ -207,7 +216,7 @@ def _fix_null_typing(
207
216
  del schema["type"]
208
217
 
209
218
 
210
- def _fix_tuple_items(schema: Dict):
219
+ def _fix_tuple_items(schema: dict[str, Any]) -> None:
211
220
  """
212
221
  Pydantic V1 does not generate a valid Draft2020-12 schema for tuples.
213
222
  """
@@ -216,15 +225,15 @@ def _fix_tuple_items(schema: Dict):
216
225
  and isinstance(schema["items"], list)
217
226
  and not schema.get("prefixItems")
218
227
  ):
219
- schema["prefixItems"] = deepcopy(schema["items"])
228
+ schema["prefixItems"] = deepcopy(cast(list[Any], schema["items"]))
220
229
  del schema["items"]
221
230
 
222
231
 
223
232
  def process_properties(
224
- properties: Dict,
225
- required_fields: List[str],
233
+ properties: dict[str, dict[str, Any]],
234
+ required_fields: list[str],
226
235
  allow_none_with_default: bool = False,
227
- ):
236
+ ) -> None:
228
237
  for key, schema in properties.items():
229
238
  _fix_null_typing(key, schema, required_fields, allow_none_with_default)
230
239
  _fix_tuple_items(schema)
@@ -235,9 +244,9 @@ def process_properties(
235
244
 
236
245
 
237
246
  def preprocess_schema(
238
- schema: Dict,
247
+ schema: ObjectSchema,
239
248
  allow_none_with_default: bool = False,
240
- ):
249
+ ) -> ObjectSchema:
241
250
  schema = deepcopy(schema)
242
251
 
243
252
  if "properties" in schema:
@@ -247,7 +256,8 @@ def preprocess_schema(
247
256
  )
248
257
 
249
258
  if "definitions" in schema: # Also process definitions for reused models
250
- for definition in (schema["definitions"] or {}).values():
259
+ definitions = cast(dict[str, Any], schema["definitions"])
260
+ for definition in definitions.values():
251
261
  if "properties" in definition:
252
262
  required_fields = definition.get("required", [])
253
263
  process_properties(
@@ -1,9 +1,10 @@
1
- import sys
2
1
  import threading
3
2
  from collections import deque
3
+ from collections.abc import Coroutine
4
+ from logging import Logger
4
5
  from traceback import format_exception
5
6
  from types import TracebackType
6
- from typing import Callable, Coroutine, Deque, Optional, Tuple
7
+ from typing import Any, Callable, Optional
7
8
  from wsgiref.simple_server import WSGIServer
8
9
 
9
10
  import anyio
@@ -14,11 +15,11 @@ from prefect.settings import PREFECT_CLIENT_METRICS_ENABLED, PREFECT_CLIENT_METR
14
15
  from prefect.utilities.collections import distinct
15
16
  from prefect.utilities.math import clamped_poisson_interval
16
17
 
17
- logger = get_logger("utilities.services.critical_service_loop")
18
+ logger: Logger = get_logger("utilities.services.critical_service_loop")
18
19
 
19
20
 
20
21
  async def critical_service_loop(
21
- workload: Callable[..., Coroutine],
22
+ workload: Callable[..., Coroutine[Any, Any, Any]],
22
23
  interval: float,
23
24
  memory: int = 10,
24
25
  consecutive: int = 3,
@@ -26,7 +27,7 @@ async def critical_service_loop(
26
27
  printer: Callable[..., None] = print,
27
28
  run_once: bool = False,
28
29
  jitter_range: Optional[float] = None,
29
- ):
30
+ ) -> None:
30
31
  """
31
32
  Runs the given `workload` function on the specified `interval`, while being
32
33
  forgiving of intermittent issues like temporary HTTP errors. If more than a certain
@@ -50,8 +51,8 @@ async def critical_service_loop(
50
51
  between `interval * (1 - range) < rv < interval * (1 + range)`
51
52
  """
52
53
 
53
- track_record: Deque[bool] = deque([True] * consecutive, maxlen=consecutive)
54
- failures: Deque[Tuple[Exception, TracebackType]] = deque(maxlen=memory)
54
+ track_record: deque[bool] = deque([True] * consecutive, maxlen=consecutive)
55
+ failures: deque[tuple[Exception, Optional[TracebackType]]] = deque(maxlen=memory)
55
56
  backoff_count = 0
56
57
 
57
58
  while True:
@@ -78,7 +79,7 @@ async def critical_service_loop(
78
79
  # or Prefect Cloud is having an outage (which will be covered by the
79
80
  # exception clause below)
80
81
  track_record.append(False)
81
- failures.append((exc, sys.exc_info()[-1]))
82
+ failures.append((exc, exc.__traceback__))
82
83
  logger.debug(
83
84
  f"Run of {workload!r} failed with TransportError", exc_info=exc
84
85
  )
@@ -88,7 +89,7 @@ async def critical_service_loop(
88
89
  # likely to be temporary and transient. Don't quit over these unless
89
90
  # it is prolonged.
90
91
  track_record.append(False)
91
- failures.append((exc, sys.exc_info()[-1]))
92
+ failures.append((exc, exc.__traceback__))
92
93
  logger.debug(
93
94
  f"Run of {workload!r} failed with HTTPStatusError", exc_info=exc
94
95
  )
@@ -155,10 +156,10 @@ async def critical_service_loop(
155
156
  await anyio.sleep(sleep)
156
157
 
157
158
 
158
- _metrics_server: Optional[Tuple[WSGIServer, threading.Thread]] = None
159
+ _metrics_server: Optional[tuple[WSGIServer, threading.Thread]] = None
159
160
 
160
161
 
161
- def start_client_metrics_server():
162
+ def start_client_metrics_server() -> None:
162
163
  """Start the process-wide Prometheus metrics server for client metrics (if enabled
163
164
  with `PREFECT_CLIENT_METRICS_ENABLED`) on the port `PREFECT_CLIENT_METRICS_PORT`."""
164
165
  if not PREFECT_CLIENT_METRICS_ENABLED:
@@ -173,7 +174,7 @@ def start_client_metrics_server():
173
174
  _metrics_server = start_http_server(port=PREFECT_CLIENT_METRICS_PORT.value())
174
175
 
175
176
 
176
- def stop_client_metrics_server():
177
+ def stop_client_metrics_server() -> None:
177
178
  """Start the process-wide Prometheus metrics server for client metrics, if it has
178
179
  previously been started"""
179
180
  global _metrics_server