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
@@ -1,10 +1,12 @@
|
|
1
1
|
import json
|
2
|
-
from
|
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:
|
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:
|
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
|
-
) ->
|
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:
|
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) ->
|
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
|
-
|
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
|
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=
|
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(
|
281
|
-
|
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
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
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
|
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
|
-
|
32
|
+
assert not isinstance(schema, bool)
|
33
|
+
schema = {**schema, PLACEHOLDERS_VALIDATOR_NAME: None}
|
34
|
+
return schema.items()
|
28
35
|
|
29
|
-
def _placeholders(
|
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=
|
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:
|
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
|
-
|
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:
|
66
|
-
schema:
|
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
|
-
) ->
|
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
|
114
|
+
return not errors
|
108
115
|
|
109
116
|
|
110
|
-
def prioritize_placeholder_errors(
|
111
|
-
|
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
|
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:
|
133
|
-
error_response:
|
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.
|
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 =
|
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:
|
194
|
-
required_fields:
|
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:
|
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:
|
225
|
-
required_fields:
|
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:
|
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
|
-
|
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(
|
prefect/utilities/services.py
CHANGED
@@ -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
|
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:
|
54
|
-
failures:
|
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,
|
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,
|
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[
|
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
|