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.
Files changed (45) hide show
  1. prefect/_internal/compatibility/deprecated.py +2 -0
  2. prefect/_internal/pydantic/_compat.py +1 -0
  3. prefect/_internal/pydantic/utilities/field_validator.py +25 -10
  4. prefect/_internal/pydantic/utilities/model_dump.py +1 -1
  5. prefect/_internal/pydantic/utilities/model_validate.py +1 -1
  6. prefect/_internal/pydantic/utilities/model_validator.py +11 -3
  7. prefect/_internal/schemas/validators.py +0 -6
  8. prefect/_version.py +97 -38
  9. prefect/blocks/abstract.py +34 -1
  10. prefect/blocks/notifications.py +14 -5
  11. prefect/client/base.py +10 -5
  12. prefect/client/orchestration.py +125 -66
  13. prefect/client/schemas/actions.py +4 -3
  14. prefect/client/schemas/objects.py +6 -5
  15. prefect/client/schemas/schedules.py +2 -6
  16. prefect/deployments/__init__.py +0 -2
  17. prefect/deployments/base.py +2 -144
  18. prefect/deployments/deployments.py +2 -2
  19. prefect/deployments/runner.py +2 -2
  20. prefect/deployments/steps/core.py +3 -3
  21. prefect/deprecated/packaging/serializers.py +5 -4
  22. prefect/events/__init__.py +45 -0
  23. prefect/events/actions.py +250 -19
  24. prefect/events/cli/__init__.py +0 -0
  25. prefect/events/cli/automations.py +163 -0
  26. prefect/events/clients.py +133 -7
  27. prefect/events/schemas/automations.py +76 -3
  28. prefect/events/schemas/deployment_triggers.py +17 -59
  29. prefect/events/utilities.py +2 -0
  30. prefect/events/worker.py +12 -2
  31. prefect/exceptions.py +1 -1
  32. prefect/logging/__init__.py +2 -2
  33. prefect/logging/loggers.py +64 -1
  34. prefect/results.py +29 -10
  35. prefect/serializers.py +62 -31
  36. prefect/settings.py +6 -10
  37. prefect/types/__init__.py +90 -0
  38. prefect/utilities/pydantic.py +34 -15
  39. prefect/utilities/schema_tools/hydration.py +88 -19
  40. prefect/variables.py +4 -4
  41. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/METADATA +1 -1
  42. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/RECORD +45 -42
  43. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/LICENSE +0 -0
  44. {prefect_client-2.17.1.dist-info → prefect_client-2.18.0.dist-info}/WHEEL +0 -0
  45. {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
- import pydantic.v1 as pydantic
30
- from pydantic.v1 import BaseModel
31
- from pydantic.v1.json import pydantic_encoder
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 pydantic
34
- from pydantic import BaseModel
35
- from pydantic.json import pydantic_encoder
36
-
37
- from typing_extensions import Literal
38
-
39
- from prefect.utilities.importtools import from_qualified_name, to_qualified_name
40
- from prefect.utilities.pydantic import add_type_dispatch
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": pydantic_encoder(obj),
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 pydantic.parse_obj_as(
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
- @add_type_dispatch
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
- @pydantic.validator("picklelib")
138
+ @validator("picklelib")
111
139
  def check_picklelib(cls, value):
112
140
  return validate_picklelib(value)
113
141
 
114
- @pydantic.root_validator
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] = pydantic.Field(
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 Pydantic."
173
+ "adds support for all types supported by "
145
174
  ),
146
175
  )
147
- object_decoder: Optional[str] = pydantic.Field(
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] = pydantic.Field(default_factory=dict)
157
- loads_kwargs: Dict[str, Any] = pydantic.Field(default_factory=dict)
185
+ dumps_kwargs: Dict[str, Any] = Field(default_factory=dict)
186
+ loads_kwargs: Dict[str, Any] = Field(default_factory=dict)
158
187
 
159
- @pydantic.validator("dumps_kwargs")
188
+ @validator("dumps_kwargs")
160
189
  def dumps_kwargs_cannot_contain_default(cls, value):
161
190
  return validate_dump_kwargs(value)
162
191
 
163
- @pydantic.validator("loads_kwargs")
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
- @pydantic.validator("serializer", pre=True)
232
+ @validator("serializer", pre=True)
204
233
  def validate_serializer(cls, value):
205
234
  return cast_type_names_to_serializers(value)
206
235
 
207
- @pydantic.validator("compressionlib")
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
- serializer: Serializer = pydantic.Field(default_factory=PickleSerializer)
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
- serializer: Serializer = pydantic.Field(default_factory=JSONSerializer)
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
+ ]
@@ -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=pydantic.BaseModel)
19
+ M = TypeVar("M", bound=pydantic_v1.BaseModel)
19
20
 
20
21
 
21
- def _reduce_model(model: pydantic.BaseModel):
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[pydantic.BaseModel]) -> set:
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, pydantic.BaseModel):
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"] = pydantic.fields.ModelField(
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 pydantic.ValidationError(errors=[exc], model=cls)
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(pydantic.BaseModel):
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
- from prefect._internal.pydantic import HAS_PYDANTIC_V2
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.models.variables import read_variables
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 = await read_variables(
28
- session=session,
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[[Dict, HydrationContext], Any]
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: Dict, ctx: HydrationContext) -> Any:
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: Dict, ctx: HydrationContext):
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: Dict, ctx: HydrationContext):
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: Dict, ctx: HydrationContext):
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: Dict, ctx: Optional[HydrationContext] = None):
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 variables
33
+ from prefect.variables import Variable
34
34
 
35
35
  @flow
36
36
  def my_flow():
37
- var = variables.Variable.set(name="my_var",value="test_value", tags=["hi", "there"], overwrite=True)
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 variables
72
+ from prefect.variables import Variable
73
73
 
74
74
  @flow
75
75
  def my_flow():
76
- var = variables.Variable.get("my_var")
76
+ var = Variable.get("my_var")
77
77
  ```
78
78
  or
79
79
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prefect-client
3
- Version: 2.17.1
3
+ Version: 2.18.0
4
4
  Summary: Workflow orchestration and management.
5
5
  Home-page: https://www.prefect.io
6
6
  Author: Prefect Technologies, Inc.