prefect-client 2.17.1__py3-none-any.whl → 2.18.0__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/_internal/compatibility/deprecated.py +2 -0
- prefect/_internal/pydantic/_compat.py +1 -0
- prefect/_internal/pydantic/utilities/field_validator.py +25 -10
- prefect/_internal/pydantic/utilities/model_dump.py +1 -1
- prefect/_internal/pydantic/utilities/model_validate.py +1 -1
- prefect/_internal/pydantic/utilities/model_validator.py +11 -3
- prefect/_internal/schemas/validators.py +0 -6
- prefect/_version.py +97 -38
- prefect/blocks/abstract.py +34 -1
- prefect/blocks/notifications.py +14 -5
- prefect/client/base.py +10 -5
- prefect/client/orchestration.py +125 -66
- prefect/client/schemas/actions.py +4 -3
- prefect/client/schemas/objects.py +6 -5
- prefect/client/schemas/schedules.py +2 -6
- prefect/deployments/__init__.py +0 -2
- prefect/deployments/base.py +2 -144
- prefect/deployments/deployments.py +2 -2
- prefect/deployments/runner.py +2 -2
- prefect/deployments/steps/core.py +3 -3
- prefect/deprecated/packaging/serializers.py +5 -4
- prefect/events/__init__.py +45 -0
- prefect/events/actions.py +250 -19
- prefect/events/cli/__init__.py +0 -0
- prefect/events/cli/automations.py +163 -0
- prefect/events/clients.py +133 -7
- prefect/events/schemas/automations.py +76 -3
- prefect/events/schemas/deployment_triggers.py +17 -59
- prefect/events/utilities.py +2 -0
- prefect/events/worker.py +12 -2
- prefect/exceptions.py +1 -1
- prefect/logging/__init__.py +2 -2
- prefect/logging/loggers.py +64 -1
- prefect/results.py +29 -10
- prefect/serializers.py +62 -31
- prefect/settings.py +6 -10
- prefect/types/__init__.py +90 -0
- prefect/utilities/pydantic.py +34 -15
- prefect/utilities/schema_tools/hydration.py +88 -19
- prefect/variables.py +4 -4
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/METADATA +1 -1
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/RECORD +45 -42
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/WHEEL +0 -0
- {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/top_level.txt +0 -0
prefect/serializers.py
CHANGED
@@ -13,9 +13,10 @@ bytes to an object respectively.
|
|
13
13
|
|
14
14
|
import abc
|
15
15
|
import base64
|
16
|
-
from typing import Any, Dict, Generic, Optional, TypeVar
|
16
|
+
from typing import Any, Dict, Generic, Optional, Type, TypeVar
|
17
|
+
|
18
|
+
from typing_extensions import Literal, Self
|
17
19
|
|
18
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
19
20
|
from prefect._internal.schemas.validators import (
|
20
21
|
cast_type_names_to_serializers,
|
21
22
|
validate_compressionlib,
|
@@ -24,20 +25,29 @@ from prefect._internal.schemas.validators import (
|
|
24
25
|
validate_picklelib,
|
25
26
|
validate_picklelib_version,
|
26
27
|
)
|
28
|
+
from prefect.pydantic import HAS_PYDANTIC_V2
|
29
|
+
from prefect.utilities.dispatch import get_dispatch_key, lookup_type, register_base_type
|
30
|
+
from prefect.utilities.importtools import from_qualified_name, to_qualified_name
|
31
|
+
from prefect.utilities.pydantic import custom_pydantic_encoder
|
27
32
|
|
28
33
|
if HAS_PYDANTIC_V2:
|
29
|
-
|
30
|
-
|
31
|
-
|
34
|
+
from pydantic.v1 import (
|
35
|
+
BaseModel,
|
36
|
+
Field,
|
37
|
+
ValidationError,
|
38
|
+
parse_obj_as,
|
39
|
+
root_validator,
|
40
|
+
validator,
|
41
|
+
)
|
32
42
|
else:
|
33
|
-
import
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
from pydantic import (
|
44
|
+
BaseModel,
|
45
|
+
Field,
|
46
|
+
ValidationError,
|
47
|
+
parse_obj_as,
|
48
|
+
root_validator,
|
49
|
+
validator,
|
50
|
+
)
|
41
51
|
|
42
52
|
D = TypeVar("D")
|
43
53
|
|
@@ -53,7 +63,7 @@ def prefect_json_object_encoder(obj: Any) -> Any:
|
|
53
63
|
else:
|
54
64
|
return {
|
55
65
|
"__class__": to_qualified_name(obj.__class__),
|
56
|
-
"data":
|
66
|
+
"data": custom_pydantic_encoder({}, obj),
|
57
67
|
}
|
58
68
|
|
59
69
|
|
@@ -63,21 +73,35 @@ def prefect_json_object_decoder(result: dict):
|
|
63
73
|
with `prefect_json_object_encoder`
|
64
74
|
"""
|
65
75
|
if "__class__" in result:
|
66
|
-
return
|
67
|
-
from_qualified_name(result["__class__"]), result["data"]
|
68
|
-
)
|
76
|
+
return parse_obj_as(from_qualified_name(result["__class__"]), result["data"])
|
69
77
|
elif "__exc_type__" in result:
|
70
78
|
return from_qualified_name(result["__exc_type__"])(result["message"])
|
71
79
|
else:
|
72
80
|
return result
|
73
81
|
|
74
82
|
|
75
|
-
@
|
83
|
+
@register_base_type
|
76
84
|
class Serializer(BaseModel, Generic[D], abc.ABC):
|
77
85
|
"""
|
78
86
|
A serializer that can encode objects of type 'D' into bytes.
|
79
87
|
"""
|
80
88
|
|
89
|
+
def __init__(self, **data: Any) -> None:
|
90
|
+
type_string = get_dispatch_key(self) if type(self) != Serializer else "__base__"
|
91
|
+
data.setdefault("type", type_string)
|
92
|
+
super().__init__(**data)
|
93
|
+
|
94
|
+
def __new__(cls: Type[Self], **kwargs) -> Self:
|
95
|
+
if "type" in kwargs:
|
96
|
+
try:
|
97
|
+
subcls = lookup_type(cls, dispatch_key=kwargs["type"])
|
98
|
+
except KeyError as exc:
|
99
|
+
raise ValidationError(errors=[exc], model=cls)
|
100
|
+
|
101
|
+
return super().__new__(subcls)
|
102
|
+
else:
|
103
|
+
return super().__new__(cls)
|
104
|
+
|
81
105
|
type: str
|
82
106
|
|
83
107
|
@abc.abstractmethod
|
@@ -91,6 +115,10 @@ class Serializer(BaseModel, Generic[D], abc.ABC):
|
|
91
115
|
class Config:
|
92
116
|
extra = "forbid"
|
93
117
|
|
118
|
+
@classmethod
|
119
|
+
def __dispatch_key__(cls):
|
120
|
+
return cls.__fields__.get("type").get_default()
|
121
|
+
|
94
122
|
|
95
123
|
class PickleSerializer(Serializer):
|
96
124
|
"""
|
@@ -107,11 +135,11 @@ class PickleSerializer(Serializer):
|
|
107
135
|
picklelib: str = "cloudpickle"
|
108
136
|
picklelib_version: str = None
|
109
137
|
|
110
|
-
@
|
138
|
+
@validator("picklelib")
|
111
139
|
def check_picklelib(cls, value):
|
112
140
|
return validate_picklelib(value)
|
113
141
|
|
114
|
-
@
|
142
|
+
@root_validator
|
115
143
|
def check_picklelib_version(cls, values):
|
116
144
|
return validate_picklelib_version(values)
|
117
145
|
|
@@ -135,16 +163,17 @@ class JSONSerializer(Serializer):
|
|
135
163
|
"""
|
136
164
|
|
137
165
|
type: Literal["json"] = "json"
|
166
|
+
|
138
167
|
jsonlib: str = "json"
|
139
|
-
object_encoder: Optional[str] =
|
168
|
+
object_encoder: Optional[str] = Field(
|
140
169
|
default="prefect.serializers.prefect_json_object_encoder",
|
141
170
|
description=(
|
142
171
|
"An optional callable to use when serializing objects that are not "
|
143
172
|
"supported by the JSON encoder. By default, this is set to a callable that "
|
144
|
-
"adds support for all types supported by
|
173
|
+
"adds support for all types supported by "
|
145
174
|
),
|
146
175
|
)
|
147
|
-
object_decoder: Optional[str] =
|
176
|
+
object_decoder: Optional[str] = Field(
|
148
177
|
default="prefect.serializers.prefect_json_object_decoder",
|
149
178
|
description=(
|
150
179
|
"An optional callable to use when deserializing objects. This callable "
|
@@ -153,14 +182,14 @@ class JSONSerializer(Serializer):
|
|
153
182
|
"by our default `object_encoder`."
|
154
183
|
),
|
155
184
|
)
|
156
|
-
dumps_kwargs: Dict[str, Any] =
|
157
|
-
loads_kwargs: Dict[str, Any] =
|
185
|
+
dumps_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
186
|
+
loads_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
158
187
|
|
159
|
-
@
|
188
|
+
@validator("dumps_kwargs")
|
160
189
|
def dumps_kwargs_cannot_contain_default(cls, value):
|
161
190
|
return validate_dump_kwargs(value)
|
162
191
|
|
163
|
-
@
|
192
|
+
@validator("loads_kwargs")
|
164
193
|
def loads_kwargs_cannot_contain_object_hook(cls, value):
|
165
194
|
return validate_load_kwargs(value)
|
166
195
|
|
@@ -200,11 +229,11 @@ class CompressedSerializer(Serializer):
|
|
200
229
|
serializer: Serializer
|
201
230
|
compressionlib: str = "lzma"
|
202
231
|
|
203
|
-
@
|
232
|
+
@validator("serializer", pre=True)
|
204
233
|
def validate_serializer(cls, value):
|
205
234
|
return cast_type_names_to_serializers(value)
|
206
235
|
|
207
|
-
@
|
236
|
+
@validator("compressionlib")
|
208
237
|
def check_compressionlib(cls, value):
|
209
238
|
return validate_compressionlib(value)
|
210
239
|
|
@@ -225,7 +254,8 @@ class CompressedPickleSerializer(CompressedSerializer):
|
|
225
254
|
"""
|
226
255
|
|
227
256
|
type: Literal["compressed/pickle"] = "compressed/pickle"
|
228
|
-
|
257
|
+
|
258
|
+
serializer: Serializer = Field(default_factory=PickleSerializer)
|
229
259
|
|
230
260
|
|
231
261
|
class CompressedJSONSerializer(CompressedSerializer):
|
@@ -234,4 +264,5 @@ class CompressedJSONSerializer(CompressedSerializer):
|
|
234
264
|
"""
|
235
265
|
|
236
266
|
type: Literal["compressed/json"] = "compressed/json"
|
237
|
-
|
267
|
+
|
268
|
+
serializer: Serializer = Field(default_factory=JSONSerializer)
|
prefect/settings.py
CHANGED
@@ -109,6 +109,8 @@ REMOVED_EXPERIMENTAL_FLAGS = {
|
|
109
109
|
"PREFECT_EXPERIMENTAL_ENABLE_ENHANCED_DEPLOYMENT_PARAMETERS",
|
110
110
|
"PREFECT_EXPERIMENTAL_ENABLE_EVENTS_CLIENT",
|
111
111
|
"PREFECT_EXPERIMENTAL_WARN_EVENTS_CLIENT",
|
112
|
+
"PREFECT_EXPERIMENTAL_ENABLE_FLOW_RUN_INFRA_OVERRIDES",
|
113
|
+
"PREFECT_EXPERIMENTAL_WARN_FLOW_RUN_INFRA_OVERRIDES",
|
112
114
|
}
|
113
115
|
|
114
116
|
|
@@ -1542,16 +1544,6 @@ a task server should move a task from PENDING to RUNNING very quickly, so runs s
|
|
1542
1544
|
PENDING for a while is a sign that the task server may have crashed.
|
1543
1545
|
"""
|
1544
1546
|
|
1545
|
-
PREFECT_EXPERIMENTAL_ENABLE_FLOW_RUN_INFRA_OVERRIDES = Setting(bool, default=False)
|
1546
|
-
"""
|
1547
|
-
Whether or not to enable infrastructure overrides made on flow runs.
|
1548
|
-
"""
|
1549
|
-
|
1550
|
-
PREFECT_EXPERIMENTAL_WARN_FLOW_RUN_INFRA_OVERRIDES = Setting(bool, default=True)
|
1551
|
-
"""
|
1552
|
-
Whether or not to warn infrastructure when experimental flow runs overrides are used.
|
1553
|
-
"""
|
1554
|
-
|
1555
1547
|
PREFECT_EXPERIMENTAL_ENABLE_EXTRA_RUNNER_ENDPOINTS = Setting(bool, default=False)
|
1556
1548
|
"""
|
1557
1549
|
Whether or not to enable experimental worker webserver endpoints.
|
@@ -1701,6 +1693,10 @@ PREFECT_API_SERVICES_EVENT_PERSISTER_FLUSH_INTERVAL = Setting(float, default=5,
|
|
1701
1693
|
The maximum number of seconds between flushes of the event persister.
|
1702
1694
|
"""
|
1703
1695
|
|
1696
|
+
PREFECT_API_EVENTS_STREAM_OUT_ENABLED = Setting(bool, default=True)
|
1697
|
+
"""
|
1698
|
+
Whether or not to allow streaming events out of via websockets.
|
1699
|
+
"""
|
1704
1700
|
|
1705
1701
|
# Deprecated settings ------------------------------------------------------------------
|
1706
1702
|
|
@@ -0,0 +1,90 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Any, Callable, ClassVar, Generator
|
3
|
+
|
4
|
+
from pydantic_core import core_schema, CoreSchema, SchemaValidator
|
5
|
+
from typing_extensions import Self
|
6
|
+
from datetime import timedelta
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class NonNegativeInteger(int):
|
11
|
+
schema: ClassVar[CoreSchema] = core_schema.int_schema(ge=0)
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
15
|
+
yield cls.validate
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def __get_pydantic_core_schema__(
|
19
|
+
cls, source_type: Any, handler: Callable[..., Any]
|
20
|
+
) -> CoreSchema:
|
21
|
+
return cls.schema
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def validate(cls, v: Any) -> Self:
|
25
|
+
return SchemaValidator(schema=cls.schema).validate_python(v)
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class PositiveInteger(int):
|
30
|
+
schema: ClassVar[CoreSchema] = core_schema.int_schema(gt=0)
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
34
|
+
yield cls.validate
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def __get_pydantic_core_schema__(
|
38
|
+
cls, source_type: Any, handler: Callable[..., Any]
|
39
|
+
) -> CoreSchema:
|
40
|
+
return cls.schema
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def validate(cls, v: Any) -> Self:
|
44
|
+
return SchemaValidator(schema=cls.schema).validate_python(v)
|
45
|
+
|
46
|
+
|
47
|
+
@dataclass
|
48
|
+
class NonNegativeDuration(timedelta):
|
49
|
+
schema: ClassVar = core_schema.timedelta_schema(ge=timedelta(seconds=0))
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
53
|
+
yield cls.validate
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def __get_pydantic_core_schema__(
|
57
|
+
cls, source_type: Any, handler: Callable[..., Any]
|
58
|
+
) -> CoreSchema:
|
59
|
+
return cls.schema
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def validate(cls, v: Any) -> Self:
|
63
|
+
return SchemaValidator(schema=cls.schema).validate_python(v)
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class PositiveDuration(timedelta):
|
68
|
+
schema: ClassVar = core_schema.timedelta_schema(gt=timedelta(seconds=0))
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
72
|
+
yield cls.validate
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def __get_pydantic_core_schema__(
|
76
|
+
cls, source_type: Any, handler: Callable[..., Any]
|
77
|
+
) -> CoreSchema:
|
78
|
+
return cls.schema
|
79
|
+
|
80
|
+
@classmethod
|
81
|
+
def validate(cls, v: Any) -> Self:
|
82
|
+
return SchemaValidator(schema=cls.schema).validate_python(v)
|
83
|
+
|
84
|
+
|
85
|
+
__all__ = [
|
86
|
+
"NonNegativeInteger",
|
87
|
+
"PositiveInteger",
|
88
|
+
"NonNegativeDuration",
|
89
|
+
"PositiveDuration",
|
90
|
+
]
|
prefect/utilities/pydantic.py
CHANGED
@@ -1,24 +1,25 @@
|
|
1
1
|
from functools import partial
|
2
|
-
from typing import Any, Callable, Generic, Type, TypeVar, cast, overload
|
3
|
-
|
4
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
5
|
-
|
6
|
-
if HAS_PYDANTIC_V2:
|
7
|
-
import pydantic.v1 as pydantic
|
8
|
-
else:
|
9
|
-
import pydantic
|
2
|
+
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, cast, overload
|
10
3
|
|
11
4
|
from jsonpatch import JsonPatch as JsonPatchBase
|
5
|
+
from pydantic_core import to_jsonable_python
|
12
6
|
from typing_extensions import Self
|
13
7
|
|
8
|
+
from prefect._internal.pydantic.utilities.model_dump import model_dump
|
9
|
+
from prefect.pydantic import HAS_PYDANTIC_V2
|
14
10
|
from prefect.utilities.dispatch import get_dispatch_key, lookup_type, register_base_type
|
15
11
|
from prefect.utilities.importtools import from_qualified_name, to_qualified_name
|
16
12
|
|
13
|
+
if HAS_PYDANTIC_V2:
|
14
|
+
import pydantic.v1 as pydantic_v1
|
15
|
+
else:
|
16
|
+
import pydantic as pydantic_v1
|
17
|
+
|
17
18
|
D = TypeVar("D", bound=Any)
|
18
|
-
M = TypeVar("M", bound=
|
19
|
+
M = TypeVar("M", bound=pydantic_v1.BaseModel)
|
19
20
|
|
20
21
|
|
21
|
-
def _reduce_model(model:
|
22
|
+
def _reduce_model(model: pydantic_v1.BaseModel):
|
22
23
|
"""
|
23
24
|
Helper for serializing a cythonized model with cloudpickle.
|
24
25
|
|
@@ -82,7 +83,7 @@ def add_cloudpickle_reduction(__model_cls: Type[M] = None, **kwargs: Any):
|
|
82
83
|
)
|
83
84
|
|
84
85
|
|
85
|
-
def get_class_fields_only(model: Type[
|
86
|
+
def get_class_fields_only(model: Type[pydantic_v1.BaseModel]) -> set:
|
86
87
|
"""
|
87
88
|
Gets all the field names defined on the model class but not any parent classes.
|
88
89
|
Any fields that are on the parent but redefined on the subclass are included.
|
@@ -91,7 +92,7 @@ def get_class_fields_only(model: Type[pydantic.BaseModel]) -> set:
|
|
91
92
|
parent_class_fields = set()
|
92
93
|
|
93
94
|
for base in model.__class__.__bases__:
|
94
|
-
if issubclass(base,
|
95
|
+
if issubclass(base, pydantic_v1.BaseModel):
|
95
96
|
parent_class_fields.update(base.__annotations__.keys())
|
96
97
|
|
97
98
|
return (subclass_class_fields - parent_class_fields) | (
|
@@ -134,7 +135,7 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
134
135
|
|
135
136
|
elif defines_dispatch_key and not defines_type_field:
|
136
137
|
# Add a type field to store the value of the dispatch key
|
137
|
-
model_cls.__fields__["type"] =
|
138
|
+
model_cls.__fields__["type"] = pydantic_v1.fields.ModelField(
|
138
139
|
name="type",
|
139
140
|
type_=str,
|
140
141
|
required=True,
|
@@ -180,7 +181,7 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
180
181
|
try:
|
181
182
|
subcls = lookup_type(cls, dispatch_key=kwargs["type"])
|
182
183
|
except KeyError as exc:
|
183
|
-
raise
|
184
|
+
raise pydantic_v1.ValidationError(errors=[exc], model=cls)
|
184
185
|
return cls_new(subcls)
|
185
186
|
else:
|
186
187
|
return cls_new(cls)
|
@@ -206,7 +207,7 @@ class PartialModel(Generic[M]):
|
|
206
207
|
a field already has a value.
|
207
208
|
|
208
209
|
Example:
|
209
|
-
>>> class MyModel(
|
210
|
+
>>> class MyModel(pydantic_v1.BaseModel):
|
210
211
|
>>> x: int
|
211
212
|
>>> y: str
|
212
213
|
>>> z: float
|
@@ -267,3 +268,21 @@ class JsonPatch(JsonPatchBase):
|
|
267
268
|
},
|
268
269
|
}
|
269
270
|
)
|
271
|
+
|
272
|
+
|
273
|
+
def custom_pydantic_encoder(
|
274
|
+
type_encoders: Optional[Dict[Any, Callable[[Type[Any]], Any]]], obj: Any
|
275
|
+
) -> Any:
|
276
|
+
# Check the class type and its superclasses for a matching encoder
|
277
|
+
for base in obj.__class__.__mro__[:-1]:
|
278
|
+
try:
|
279
|
+
encoder = type_encoders[base]
|
280
|
+
except KeyError:
|
281
|
+
continue
|
282
|
+
|
283
|
+
return encoder(obj)
|
284
|
+
else: # We have exited the for loop without finding a suitable encoder
|
285
|
+
if isinstance(obj, pydantic_v1.BaseModel):
|
286
|
+
return model_dump(obj, mode="json")
|
287
|
+
else:
|
288
|
+
return to_jsonable_python(obj)
|
@@ -1,41 +1,53 @@
|
|
1
1
|
import json
|
2
2
|
from typing import Any, Callable, Dict, Optional
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
if HAS_PYDANTIC_V2:
|
7
|
-
from pydantic.v1 import BaseModel, Field
|
8
|
-
else:
|
9
|
-
from pydantic import BaseModel, Field
|
10
|
-
|
4
|
+
import jinja2
|
5
|
+
from pydantic import BaseModel, Field
|
11
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
12
7
|
from typing_extensions import TypeAlias
|
13
8
|
|
14
|
-
from prefect.server.
|
9
|
+
from prefect.server.utilities.user_templates import (
|
10
|
+
TemplateSecurityError,
|
11
|
+
render_user_template_sync,
|
12
|
+
validate_user_template,
|
13
|
+
)
|
15
14
|
|
16
15
|
|
17
16
|
class HydrationContext(BaseModel):
|
18
17
|
workspace_variables: Dict[str, str] = Field(default_factory=dict)
|
18
|
+
render_workspace_variables: bool = Field(default=False)
|
19
19
|
raise_on_error: bool = Field(default=False)
|
20
|
+
render_jinja: bool = Field(default=False)
|
21
|
+
jinja_context: Dict[str, Any] = Field(default_factory=dict)
|
20
22
|
|
21
23
|
@classmethod
|
22
24
|
async def build(
|
23
25
|
cls,
|
24
26
|
session: AsyncSession,
|
25
27
|
raise_on_error: bool = False,
|
28
|
+
render_jinja: bool = False,
|
29
|
+
render_workspace_variables: bool = False,
|
26
30
|
) -> "HydrationContext":
|
27
|
-
variables
|
28
|
-
|
29
|
-
|
31
|
+
from prefect.server.models.variables import read_variables
|
32
|
+
|
33
|
+
if render_workspace_variables:
|
34
|
+
variables = await read_variables(
|
35
|
+
session=session,
|
36
|
+
)
|
37
|
+
else:
|
38
|
+
variables = []
|
39
|
+
|
30
40
|
return cls(
|
31
41
|
workspace_variables={
|
32
42
|
variable.name: variable.value for variable in variables
|
33
43
|
},
|
34
44
|
raise_on_error=raise_on_error,
|
45
|
+
render_jinja=render_jinja,
|
46
|
+
render_workspace_variables=render_workspace_variables,
|
35
47
|
)
|
36
48
|
|
37
49
|
|
38
|
-
Handler: TypeAlias = Callable[[
|
50
|
+
Handler: TypeAlias = Callable[[dict, HydrationContext], Any]
|
39
51
|
PrefectKind: TypeAlias = Optional[str]
|
40
52
|
|
41
53
|
_handlers: Dict[PrefectKind, Handler] = {}
|
@@ -93,6 +105,12 @@ class ValueNotFound(KeyNotFound):
|
|
93
105
|
return "value"
|
94
106
|
|
95
107
|
|
108
|
+
class TemplateNotFound(KeyNotFound):
|
109
|
+
@property
|
110
|
+
def key(self):
|
111
|
+
return "template"
|
112
|
+
|
113
|
+
|
96
114
|
class VariableNameNotFound(KeyNotFound):
|
97
115
|
@property
|
98
116
|
def key(self):
|
@@ -108,6 +126,15 @@ class InvalidJSON(HydrationError):
|
|
108
126
|
return message
|
109
127
|
|
110
128
|
|
129
|
+
class InvalidJinja(HydrationError):
|
130
|
+
@property
|
131
|
+
def message(self):
|
132
|
+
message = "Invalid jinja"
|
133
|
+
if self.detail:
|
134
|
+
message += f": {self.detail}"
|
135
|
+
return message
|
136
|
+
|
137
|
+
|
111
138
|
class WorkspaceVariableNotFound(HydrationError):
|
112
139
|
@property
|
113
140
|
def variable_name(self) -> str:
|
@@ -116,7 +143,25 @@ class WorkspaceVariableNotFound(HydrationError):
|
|
116
143
|
|
117
144
|
@property
|
118
145
|
def message(self):
|
119
|
-
return f"Variable '{self.detail}' not found."
|
146
|
+
return f"Variable '{self.detail}' not found in workspace."
|
147
|
+
|
148
|
+
|
149
|
+
class WorkspaceVariable(Placeholder):
|
150
|
+
def __init__(self, variable_name: str):
|
151
|
+
self.variable_name = variable_name
|
152
|
+
|
153
|
+
def __eq__(self, other):
|
154
|
+
return (
|
155
|
+
isinstance(other, type(self)) and self.variable_name == other.variable_name
|
156
|
+
)
|
157
|
+
|
158
|
+
|
159
|
+
class ValidJinja(Placeholder):
|
160
|
+
def __init__(self, template: str):
|
161
|
+
self.template = template
|
162
|
+
|
163
|
+
def __eq__(self, other):
|
164
|
+
return isinstance(other, type(self)) and self.template == other.template
|
120
165
|
|
121
166
|
|
122
167
|
def handler(kind: PrefectKind) -> Callable:
|
@@ -127,7 +172,7 @@ def handler(kind: PrefectKind) -> Callable:
|
|
127
172
|
return decorator
|
128
173
|
|
129
174
|
|
130
|
-
def call_handler(kind: PrefectKind, obj:
|
175
|
+
def call_handler(kind: PrefectKind, obj: dict, ctx: HydrationContext) -> Any:
|
131
176
|
if kind not in _handlers:
|
132
177
|
return (obj or {}).get("value", None)
|
133
178
|
|
@@ -138,7 +183,7 @@ def call_handler(kind: PrefectKind, obj: Dict, ctx: HydrationContext) -> Any:
|
|
138
183
|
|
139
184
|
|
140
185
|
@handler("none")
|
141
|
-
def null_handler(obj:
|
186
|
+
def null_handler(obj: dict, ctx: HydrationContext):
|
142
187
|
if "value" in obj:
|
143
188
|
# null handler is a pass through, so we want to continue to hydrate
|
144
189
|
return _hydrate(obj["value"], ctx)
|
@@ -147,7 +192,7 @@ def null_handler(obj: Dict, ctx: HydrationContext):
|
|
147
192
|
|
148
193
|
|
149
194
|
@handler("json")
|
150
|
-
def json_handler(obj:
|
195
|
+
def json_handler(obj: dict, ctx: HydrationContext):
|
151
196
|
if "value" in obj:
|
152
197
|
if isinstance(obj["value"], dict):
|
153
198
|
dehydrated_json = _hydrate(obj["value"], ctx)
|
@@ -167,14 +212,38 @@ def json_handler(obj: Dict, ctx: HydrationContext):
|
|
167
212
|
return RemoveValue()
|
168
213
|
|
169
214
|
|
215
|
+
@handler("jinja")
|
216
|
+
def jinja_handler(obj: dict, ctx: HydrationContext):
|
217
|
+
if "template" in obj:
|
218
|
+
if isinstance(obj["template"], dict):
|
219
|
+
dehydrated_jinja = _hydrate(obj["template"], ctx)
|
220
|
+
else:
|
221
|
+
dehydrated_jinja = obj["template"]
|
222
|
+
|
223
|
+
try:
|
224
|
+
validate_user_template(dehydrated_jinja)
|
225
|
+
except (jinja2.exceptions.TemplateSyntaxError, TemplateSecurityError) as exc:
|
226
|
+
return InvalidJinja(detail=str(exc))
|
227
|
+
|
228
|
+
if ctx.render_jinja:
|
229
|
+
return render_user_template_sync(dehydrated_jinja, ctx.jinja_context)
|
230
|
+
else:
|
231
|
+
return ValidJinja(template=dehydrated_jinja)
|
232
|
+
else:
|
233
|
+
return TemplateNotFound()
|
234
|
+
|
235
|
+
|
170
236
|
@handler("workspace_variable")
|
171
|
-
def workspace_variable_handler(obj:
|
237
|
+
def workspace_variable_handler(obj: dict, ctx: HydrationContext):
|
172
238
|
if "variable_name" in obj:
|
173
239
|
if isinstance(obj["variable_name"], dict):
|
174
240
|
dehydrated_variable = _hydrate(obj["variable_name"], ctx)
|
175
241
|
else:
|
176
242
|
dehydrated_variable = obj["variable_name"]
|
177
243
|
|
244
|
+
if not ctx.render_workspace_variables:
|
245
|
+
return WorkspaceVariable(variable_name=obj["variable_name"])
|
246
|
+
|
178
247
|
if dehydrated_variable in ctx.workspace_variables:
|
179
248
|
return ctx.workspace_variables[dehydrated_variable]
|
180
249
|
else:
|
@@ -191,7 +260,7 @@ def workspace_variable_handler(obj: Dict, ctx: HydrationContext):
|
|
191
260
|
return RemoveValue()
|
192
261
|
|
193
262
|
|
194
|
-
def hydrate(obj:
|
263
|
+
def hydrate(obj: dict, ctx: Optional[HydrationContext] = None):
|
195
264
|
res = _hydrate(obj, ctx)
|
196
265
|
|
197
266
|
if _remove_value(res):
|
@@ -200,7 +269,7 @@ def hydrate(obj: Dict, ctx: Optional[HydrationContext] = None):
|
|
200
269
|
return res
|
201
270
|
|
202
271
|
|
203
|
-
def _hydrate(obj, ctx: Optional[HydrationContext] = None):
|
272
|
+
def _hydrate(obj, ctx: Optional[HydrationContext] = None) -> Any:
|
204
273
|
if ctx is None:
|
205
274
|
ctx = HydrationContext()
|
206
275
|
|
prefect/variables.py
CHANGED
@@ -30,11 +30,11 @@ class Variable(VariableRequest):
|
|
30
30
|
"""
|
31
31
|
Sets a new variable. If one exists with the same name, user must pass `overwrite=True`
|
32
32
|
```
|
33
|
-
from prefect import
|
33
|
+
from prefect.variables import Variable
|
34
34
|
|
35
35
|
@flow
|
36
36
|
def my_flow():
|
37
|
-
var =
|
37
|
+
var = Variable.set(name="my_var",value="test_value", tags=["hi", "there"], overwrite=True)
|
38
38
|
```
|
39
39
|
or
|
40
40
|
```
|
@@ -69,11 +69,11 @@ class Variable(VariableRequest):
|
|
69
69
|
"""
|
70
70
|
Get a variable by name. If doesn't exist return the default.
|
71
71
|
```
|
72
|
-
from prefect import
|
72
|
+
from prefect.variables import Variable
|
73
73
|
|
74
74
|
@flow
|
75
75
|
def my_flow():
|
76
|
-
var =
|
76
|
+
var = Variable.get("my_var")
|
77
77
|
```
|
78
78
|
or
|
79
79
|
```
|