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
@@ -1,4 +1,5 @@
|
|
1
1
|
import abc
|
2
|
+
import textwrap
|
2
3
|
from datetime import timedelta
|
3
4
|
from enum import Enum
|
4
5
|
from typing import (
|
@@ -45,6 +46,10 @@ class Trigger(PrefectBaseModel, abc.ABC, extra="ignore"):
|
|
45
46
|
|
46
47
|
type: str
|
47
48
|
|
49
|
+
@abc.abstractmethod
|
50
|
+
def describe_for_cli(self, indent: int = 0) -> str:
|
51
|
+
"""Return a human-readable description of this trigger for the CLI"""
|
52
|
+
|
48
53
|
|
49
54
|
class ResourceTrigger(Trigger, abc.ABC):
|
50
55
|
"""
|
@@ -101,7 +106,7 @@ class EventTrigger(ResourceTrigger):
|
|
101
106
|
),
|
102
107
|
)
|
103
108
|
posture: Literal[Posture.Reactive, Posture.Proactive] = Field( # type: ignore[valid-type]
|
104
|
-
|
109
|
+
Posture.Reactive,
|
105
110
|
description=(
|
106
111
|
"The posture of this trigger, either Reactive or Proactive. Reactive "
|
107
112
|
"triggers respond to the _presence_ of the expected events, while "
|
@@ -148,6 +153,28 @@ class EventTrigger(ResourceTrigger):
|
|
148
153
|
|
149
154
|
return values
|
150
155
|
|
156
|
+
def describe_for_cli(self, indent: int = 0) -> str:
|
157
|
+
"""Return a human-readable description of this trigger for the CLI"""
|
158
|
+
if self.posture == Posture.Reactive:
|
159
|
+
return textwrap.indent(
|
160
|
+
"\n".join(
|
161
|
+
[
|
162
|
+
f"Reactive: expecting {self.threshold} of {self.expect}",
|
163
|
+
],
|
164
|
+
),
|
165
|
+
prefix=" " * indent,
|
166
|
+
)
|
167
|
+
else:
|
168
|
+
return textwrap.indent(
|
169
|
+
"\n".join(
|
170
|
+
[
|
171
|
+
f"Proactive: expecting {self.threshold} {self.expect} event "
|
172
|
+
f"within {self.within}",
|
173
|
+
],
|
174
|
+
),
|
175
|
+
prefix=" " * indent,
|
176
|
+
)
|
177
|
+
|
151
178
|
|
152
179
|
class MetricTriggerOperator(Enum):
|
153
180
|
LT = "<"
|
@@ -224,6 +251,18 @@ class MetricTrigger(ResourceTrigger):
|
|
224
251
|
description="The metric query to evaluate for this trigger. ",
|
225
252
|
)
|
226
253
|
|
254
|
+
def describe_for_cli(self, indent: int = 0) -> str:
|
255
|
+
"""Return a human-readable description of this trigger for the CLI"""
|
256
|
+
m = self.metric
|
257
|
+
return textwrap.indent(
|
258
|
+
"\n".join(
|
259
|
+
[
|
260
|
+
f"Metric: {m.name.value} {m.operator.value} {m.threshold} for {m.range}",
|
261
|
+
]
|
262
|
+
),
|
263
|
+
prefix=" " * indent,
|
264
|
+
)
|
265
|
+
|
227
266
|
|
228
267
|
class CompositeTrigger(Trigger, abc.ABC):
|
229
268
|
"""
|
@@ -256,6 +295,23 @@ class CompoundTrigger(CompositeTrigger):
|
|
256
295
|
|
257
296
|
return values
|
258
297
|
|
298
|
+
def describe_for_cli(self, indent: int = 0) -> str:
|
299
|
+
"""Return a human-readable description of this trigger for the CLI"""
|
300
|
+
return textwrap.indent(
|
301
|
+
"\n".join(
|
302
|
+
[
|
303
|
+
f"{str(self.require).capitalize()} of:",
|
304
|
+
"\n".join(
|
305
|
+
[
|
306
|
+
trigger.describe_for_cli(indent=indent + 1)
|
307
|
+
for trigger in self.triggers
|
308
|
+
]
|
309
|
+
),
|
310
|
+
]
|
311
|
+
),
|
312
|
+
prefix=" " * indent,
|
313
|
+
)
|
314
|
+
|
259
315
|
|
260
316
|
class SequenceTrigger(CompositeTrigger):
|
261
317
|
"""A composite trigger that requires some number of triggers to have fired
|
@@ -263,6 +319,23 @@ class SequenceTrigger(CompositeTrigger):
|
|
263
319
|
|
264
320
|
type: Literal["sequence"] = "sequence"
|
265
321
|
|
322
|
+
def describe_for_cli(self, indent: int = 0) -> str:
|
323
|
+
"""Return a human-readable description of this trigger for the CLI"""
|
324
|
+
return textwrap.indent(
|
325
|
+
"\n".join(
|
326
|
+
[
|
327
|
+
"In this order:",
|
328
|
+
"\n".join(
|
329
|
+
[
|
330
|
+
trigger.describe_for_cli(indent=indent + 1)
|
331
|
+
for trigger in self.triggers
|
332
|
+
]
|
333
|
+
),
|
334
|
+
]
|
335
|
+
),
|
336
|
+
prefix=" " * indent,
|
337
|
+
)
|
338
|
+
|
266
339
|
|
267
340
|
TriggerTypes: TypeAlias = Union[
|
268
341
|
EventTrigger, MetricTrigger, CompoundTrigger, SequenceTrigger
|
@@ -273,7 +346,7 @@ CompoundTrigger.update_forward_refs()
|
|
273
346
|
SequenceTrigger.update_forward_refs()
|
274
347
|
|
275
348
|
|
276
|
-
class
|
349
|
+
class AutomationCore(PrefectBaseModel, extra="ignore"):
|
277
350
|
"""Defines an action a user wants to take when a certain number of events
|
278
351
|
do or don't happen to the matching resources"""
|
279
352
|
|
@@ -310,5 +383,5 @@ class Automation(PrefectBaseModel, extra="ignore"):
|
|
310
383
|
)
|
311
384
|
|
312
385
|
|
313
|
-
class
|
386
|
+
class Automation(AutomationCore):
|
314
387
|
id: UUID = Field(..., description="The ID of this automation")
|
@@ -11,7 +11,6 @@ create them from YAML.
|
|
11
11
|
|
12
12
|
import abc
|
13
13
|
import textwrap
|
14
|
-
import warnings
|
15
14
|
from datetime import timedelta
|
16
15
|
from typing import (
|
17
16
|
Any,
|
@@ -37,20 +36,11 @@ else:
|
|
37
36
|
from pydantic import Field, PrivateAttr, root_validator, validator
|
38
37
|
from pydantic.fields import ModelField
|
39
38
|
|
40
|
-
from prefect._internal.compatibility.experimental import (
|
41
|
-
EXPERIMENTAL_WARNING,
|
42
|
-
PREFECT_EXPERIMENTAL_WARN,
|
43
|
-
ExperimentalFeature,
|
44
|
-
experiment_enabled,
|
45
|
-
)
|
46
39
|
from prefect._internal.schemas.bases import PrefectBaseModel
|
47
40
|
from prefect.events.actions import RunDeployment
|
48
|
-
from prefect.settings import (
|
49
|
-
PREFECT_EXPERIMENTAL_WARN_FLOW_RUN_INFRA_OVERRIDES,
|
50
|
-
)
|
51
41
|
|
52
42
|
from .automations import (
|
53
|
-
|
43
|
+
AutomationCore,
|
54
44
|
CompoundTrigger,
|
55
45
|
EventTrigger,
|
56
46
|
MetricTrigger,
|
@@ -106,26 +96,6 @@ class BaseDeploymentTrigger(PrefectBaseModel, abc.ABC, extra="ignore"):
|
|
106
96
|
return f"prefect.deployment.{self._deployment_id}"
|
107
97
|
|
108
98
|
def actions(self) -> List[RunDeployment]:
|
109
|
-
if self.job_variables is not None and experiment_enabled(
|
110
|
-
"flow_run_infra_overrides"
|
111
|
-
):
|
112
|
-
if (
|
113
|
-
PREFECT_EXPERIMENTAL_WARN
|
114
|
-
and PREFECT_EXPERIMENTAL_WARN_FLOW_RUN_INFRA_OVERRIDES
|
115
|
-
):
|
116
|
-
warnings.warn(
|
117
|
-
EXPERIMENTAL_WARNING.format(
|
118
|
-
feature="Flow run job variables",
|
119
|
-
group="flow_run_infra_overrides",
|
120
|
-
help="To use this feature, update your workers to Prefect 2.16.4 or later. ",
|
121
|
-
),
|
122
|
-
ExperimentalFeature,
|
123
|
-
stacklevel=3,
|
124
|
-
)
|
125
|
-
if not experiment_enabled("flow_run_infra_overrides"):
|
126
|
-
# nullify job_variables if the flag is disabled
|
127
|
-
self.job_variables = None
|
128
|
-
|
129
99
|
assert self._deployment_id
|
130
100
|
return [
|
131
101
|
RunDeployment(
|
@@ -135,11 +105,11 @@ class BaseDeploymentTrigger(PrefectBaseModel, abc.ABC, extra="ignore"):
|
|
135
105
|
)
|
136
106
|
]
|
137
107
|
|
138
|
-
def as_automation(self) ->
|
108
|
+
def as_automation(self) -> AutomationCore:
|
139
109
|
if not self.name:
|
140
110
|
raise ValueError("name is required")
|
141
111
|
|
142
|
-
return
|
112
|
+
return AutomationCore(
|
143
113
|
name=self.name,
|
144
114
|
description=self.description,
|
145
115
|
enabled=self.enabled,
|
@@ -176,6 +146,8 @@ class DeploymentEventTrigger(DeploymentResourceTrigger):
|
|
176
146
|
period of time.
|
177
147
|
"""
|
178
148
|
|
149
|
+
trigger_type = EventTrigger
|
150
|
+
|
179
151
|
type: Literal["event"] = "event"
|
180
152
|
|
181
153
|
after: Set[str] = Field(
|
@@ -256,7 +228,7 @@ class DeploymentEventTrigger(DeploymentResourceTrigger):
|
|
256
228
|
return values
|
257
229
|
|
258
230
|
def as_trigger(self) -> Trigger:
|
259
|
-
return
|
231
|
+
return self.trigger_type(
|
260
232
|
match=self.match,
|
261
233
|
match_related=self.match_related,
|
262
234
|
after=self.after,
|
@@ -273,6 +245,8 @@ class DeploymentMetricTrigger(DeploymentResourceTrigger):
|
|
273
245
|
A trigger that fires based on the results of a metric query.
|
274
246
|
"""
|
275
247
|
|
248
|
+
trigger_type = MetricTrigger
|
249
|
+
|
276
250
|
type: Literal["metric"] = "metric"
|
277
251
|
|
278
252
|
posture: Literal[Posture.Metric] = Field( # type: ignore[valid-type]
|
@@ -286,7 +260,7 @@ class DeploymentMetricTrigger(DeploymentResourceTrigger):
|
|
286
260
|
)
|
287
261
|
|
288
262
|
def as_trigger(self) -> Trigger:
|
289
|
-
return
|
263
|
+
return self.trigger_type(
|
290
264
|
match=self.match,
|
291
265
|
match_related=self.match_related,
|
292
266
|
posture=self.posture,
|
@@ -309,6 +283,8 @@ class DeploymentCompoundTrigger(DeploymentCompositeTrigger):
|
|
309
283
|
"""A composite trigger that requires some number of triggers to have
|
310
284
|
fired within the given time period"""
|
311
285
|
|
286
|
+
trigger_type = CompoundTrigger
|
287
|
+
|
312
288
|
type: Literal["compound"] = "compound"
|
313
289
|
require: Union[int, Literal["any", "all"]]
|
314
290
|
|
@@ -327,7 +303,7 @@ class DeploymentCompoundTrigger(DeploymentCompositeTrigger):
|
|
327
303
|
return values
|
328
304
|
|
329
305
|
def as_trigger(self) -> Trigger:
|
330
|
-
return
|
306
|
+
return self.trigger_type(
|
331
307
|
require=self.require,
|
332
308
|
triggers=self.triggers,
|
333
309
|
within=self.within,
|
@@ -339,10 +315,12 @@ class DeploymentSequenceTrigger(DeploymentCompositeTrigger):
|
|
339
315
|
"""A composite trigger that requires some number of triggers to have fired
|
340
316
|
within the given time period in a specific order"""
|
341
317
|
|
318
|
+
trigger_type = SequenceTrigger
|
319
|
+
|
342
320
|
type: Literal["sequence"] = "sequence"
|
343
321
|
|
344
322
|
def as_trigger(self) -> Trigger:
|
345
|
-
return
|
323
|
+
return self.trigger_type(
|
346
324
|
triggers=self.triggers,
|
347
325
|
within=self.within,
|
348
326
|
job_variables=self.job_variables,
|
@@ -480,7 +458,7 @@ class DeploymentTrigger(PrefectBaseModel):
|
|
480
458
|
),
|
481
459
|
)
|
482
460
|
|
483
|
-
def as_automation(self) ->
|
461
|
+
def as_automation(self) -> AutomationCore:
|
484
462
|
assert self.name
|
485
463
|
|
486
464
|
if self.posture == Posture.Metric:
|
@@ -503,7 +481,7 @@ class DeploymentTrigger(PrefectBaseModel):
|
|
503
481
|
within=self.within,
|
504
482
|
)
|
505
483
|
|
506
|
-
return
|
484
|
+
return AutomationCore(
|
507
485
|
name=self.name,
|
508
486
|
description=self.description,
|
509
487
|
enabled=self.enabled,
|
@@ -519,26 +497,6 @@ class DeploymentTrigger(PrefectBaseModel):
|
|
519
497
|
return f"prefect.deployment.{self._deployment_id}"
|
520
498
|
|
521
499
|
def actions(self) -> List[RunDeployment]:
|
522
|
-
if self.job_variables is not None and experiment_enabled(
|
523
|
-
"flow_run_infra_overrides"
|
524
|
-
):
|
525
|
-
if (
|
526
|
-
PREFECT_EXPERIMENTAL_WARN
|
527
|
-
and PREFECT_EXPERIMENTAL_WARN_FLOW_RUN_INFRA_OVERRIDES
|
528
|
-
):
|
529
|
-
warnings.warn(
|
530
|
-
EXPERIMENTAL_WARNING.format(
|
531
|
-
feature="Flow run job variables",
|
532
|
-
group="flow_run_infra_overrides",
|
533
|
-
help="To use this feature, update your workers to Prefect 2.16.4 or later. ",
|
534
|
-
),
|
535
|
-
ExperimentalFeature,
|
536
|
-
stacklevel=3,
|
537
|
-
)
|
538
|
-
if not experiment_enabled("flow_run_infra_overrides"):
|
539
|
-
# nullify job_variables if the flag is disabled
|
540
|
-
self.job_variables = None
|
541
|
-
|
542
500
|
assert self._deployment_id
|
543
501
|
return [
|
544
502
|
RunDeployment(
|
prefect/events/utilities.py
CHANGED
@@ -9,6 +9,7 @@ from prefect._internal.schemas.fields import DateTimeTZ
|
|
9
9
|
from .clients import (
|
10
10
|
AssertingEventsClient,
|
11
11
|
PrefectCloudEventsClient,
|
12
|
+
PrefectEphemeralEventsClient,
|
12
13
|
PrefectEventsClient,
|
13
14
|
)
|
14
15
|
from .schemas.events import Event, RelatedResource
|
@@ -53,6 +54,7 @@ def emit_event(
|
|
53
54
|
AssertingEventsClient,
|
54
55
|
PrefectCloudEventsClient,
|
55
56
|
PrefectEventsClient,
|
57
|
+
PrefectEphemeralEventsClient,
|
56
58
|
]
|
57
59
|
worker_instance = EventsWorker.instance()
|
58
60
|
|
prefect/events/worker.py
CHANGED
@@ -17,6 +17,7 @@ from .clients import (
|
|
17
17
|
EventsClient,
|
18
18
|
NullEventsClient,
|
19
19
|
PrefectCloudEventsClient,
|
20
|
+
PrefectEphemeralEventsClient,
|
20
21
|
PrefectEventsClient,
|
21
22
|
)
|
22
23
|
from .related import related_resources_from_run_context
|
@@ -24,7 +25,11 @@ from .schemas.events import Event
|
|
24
25
|
|
25
26
|
|
26
27
|
def should_emit_events() -> bool:
|
27
|
-
return
|
28
|
+
return (
|
29
|
+
emit_events_to_cloud()
|
30
|
+
or should_emit_events_to_running_server()
|
31
|
+
or should_emit_events_to_ephemeral_server()
|
32
|
+
)
|
28
33
|
|
29
34
|
|
30
35
|
def emit_events_to_cloud() -> bool:
|
@@ -39,6 +44,10 @@ def should_emit_events_to_running_server() -> bool:
|
|
39
44
|
return isinstance(api_url, str) and PREFECT_EXPERIMENTAL_EVENTS
|
40
45
|
|
41
46
|
|
47
|
+
def should_emit_events_to_ephemeral_server() -> bool:
|
48
|
+
return PREFECT_API_KEY.value() is None and PREFECT_EXPERIMENTAL_EVENTS
|
49
|
+
|
50
|
+
|
42
51
|
class EventsWorker(QueueService[Event]):
|
43
52
|
def __init__(
|
44
53
|
self, client_type: Type[EventsClient], client_options: Tuple[Tuple[str, Any]]
|
@@ -85,7 +94,8 @@ class EventsWorker(QueueService[Event]):
|
|
85
94
|
}
|
86
95
|
elif should_emit_events_to_running_server():
|
87
96
|
client_type = PrefectEventsClient
|
88
|
-
|
97
|
+
elif should_emit_events_to_ephemeral_server():
|
98
|
+
client_type = PrefectEphemeralEventsClient
|
89
99
|
else:
|
90
100
|
client_type = NullEventsClient
|
91
101
|
|
prefect/exceptions.py
CHANGED
@@ -178,7 +178,7 @@ class ParameterTypeError(PrefectException):
|
|
178
178
|
|
179
179
|
@classmethod
|
180
180
|
def from_validation_error(cls, exc: ValidationError) -> Self:
|
181
|
-
bad_params = [f'{err["loc"]
|
181
|
+
bad_params = [f'{".".join(err["loc"])}: {err["msg"]}' for err in exc.errors()]
|
182
182
|
msg = "Flow run received invalid parameters:\n - " + "\n - ".join(bad_params)
|
183
183
|
return cls(msg)
|
184
184
|
|
prefect/logging/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
from .loggers import disable_run_logger, get_logger, get_run_logger
|
1
|
+
from .loggers import disable_run_logger, get_logger, get_run_logger, LogEavesdropper
|
2
2
|
|
3
|
-
__all__ = ["get_logger", "get_run_logger"]
|
3
|
+
__all__ = ["get_logger", "get_run_logger", "LogEavesdropper"]
|
prefect/logging/loggers.py
CHANGED
@@ -5,7 +5,10 @@ import warnings
|
|
5
5
|
from builtins import print
|
6
6
|
from contextlib import contextmanager
|
7
7
|
from functools import lru_cache
|
8
|
-
from
|
8
|
+
from logging import LogRecord
|
9
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
10
|
+
|
11
|
+
from typing_extensions import Self
|
9
12
|
|
10
13
|
import prefect
|
11
14
|
from prefect.exceptions import MissingContextError
|
@@ -295,3 +298,63 @@ def patch_print():
|
|
295
298
|
yield
|
296
299
|
finally:
|
297
300
|
builtins.print = original
|
301
|
+
|
302
|
+
|
303
|
+
class LogEavesdropper(logging.Handler):
|
304
|
+
"""A context manager that collects logs for the duration of the context
|
305
|
+
|
306
|
+
Example:
|
307
|
+
|
308
|
+
```python
|
309
|
+
import logging
|
310
|
+
from prefect.logging import LogEavesdropper
|
311
|
+
|
312
|
+
with LogEavesdropper("my_logger") as eavesdropper:
|
313
|
+
logging.getLogger("my_logger").info("Hello, world!")
|
314
|
+
logging.getLogger("my_logger.child_module").info("Another one!")
|
315
|
+
|
316
|
+
print(eavesdropper.text())
|
317
|
+
|
318
|
+
# Outputs: "Hello, world!\nAnother one!"
|
319
|
+
"""
|
320
|
+
|
321
|
+
_target_logger: logging.Logger
|
322
|
+
_lines: List[str]
|
323
|
+
|
324
|
+
def __init__(self, eavesdrop_on: str, level: int = logging.NOTSET):
|
325
|
+
"""
|
326
|
+
Args:
|
327
|
+
eavesdrop_on (str): the name of the logger to eavesdrop on
|
328
|
+
level (int): the minimum log level to eavesdrop on; if omitted, all levels
|
329
|
+
are captured
|
330
|
+
"""
|
331
|
+
|
332
|
+
super().__init__(level=level)
|
333
|
+
self.eavesdrop_on = eavesdrop_on
|
334
|
+
self._target_logger = None
|
335
|
+
|
336
|
+
# It's important that we use a very minimalistic formatter for use cases where
|
337
|
+
# we may present these logs back to the user. We shouldn't leak filenames,
|
338
|
+
# versions, or other environmental information.
|
339
|
+
self.formatter = logging.Formatter("[%(levelname)s]: %(message)s")
|
340
|
+
|
341
|
+
def __enter__(self) -> Self:
|
342
|
+
self._target_logger = logging.getLogger(self.eavesdrop_on)
|
343
|
+
self._original_level = self._target_logger.level
|
344
|
+
self._target_logger.level = self.level
|
345
|
+
self._target_logger.addHandler(self)
|
346
|
+
self._lines = []
|
347
|
+
return self
|
348
|
+
|
349
|
+
def __exit__(self, *_):
|
350
|
+
if self._target_logger:
|
351
|
+
self._target_logger.removeHandler(self)
|
352
|
+
self._target_logger.level = self._original_level
|
353
|
+
|
354
|
+
def emit(self, record: LogRecord) -> None:
|
355
|
+
"""The logging.Handler implementation, not intended to be called directly."""
|
356
|
+
self._lines.append(self.format(record))
|
357
|
+
|
358
|
+
def text(self) -> str:
|
359
|
+
"""Return the collected logs as a single newline-delimited string"""
|
360
|
+
return "\n".join(self._lines)
|
prefect/results.py
CHANGED
@@ -20,13 +20,6 @@ from typing_extensions import Self
|
|
20
20
|
|
21
21
|
import prefect
|
22
22
|
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
23
|
-
|
24
|
-
if HAS_PYDANTIC_V2:
|
25
|
-
import pydantic.v1 as pydantic
|
26
|
-
|
27
|
-
else:
|
28
|
-
import pydantic
|
29
|
-
|
30
23
|
from prefect.blocks.core import Block
|
31
24
|
from prefect.client.utilities import inject_client
|
32
25
|
from prefect.exceptions import MissingResult
|
@@ -46,7 +39,14 @@ from prefect.settings import (
|
|
46
39
|
)
|
47
40
|
from prefect.utilities.annotations import NotSet
|
48
41
|
from prefect.utilities.asyncutils import sync_compatible
|
49
|
-
from prefect.utilities.pydantic import
|
42
|
+
from prefect.utilities.pydantic import get_dispatch_key, lookup_type, register_base_type
|
43
|
+
|
44
|
+
if HAS_PYDANTIC_V2:
|
45
|
+
import pydantic.v1 as pydantic
|
46
|
+
|
47
|
+
else:
|
48
|
+
import pydantic
|
49
|
+
|
50
50
|
|
51
51
|
if TYPE_CHECKING:
|
52
52
|
from prefect import Flow, Task
|
@@ -480,12 +480,27 @@ class ResultFactory(pydantic.BaseModel):
|
|
480
480
|
return self.serializer.loads(blob.data)
|
481
481
|
|
482
482
|
|
483
|
-
@
|
483
|
+
@register_base_type
|
484
484
|
class BaseResult(pydantic.BaseModel, abc.ABC, Generic[R]):
|
485
485
|
type: str
|
486
486
|
artifact_type: Optional[str]
|
487
487
|
artifact_description: Optional[str]
|
488
488
|
|
489
|
+
def __init__(self, **data: Any) -> None:
|
490
|
+
type_string = get_dispatch_key(self) if type(self) != BaseResult else "__base__"
|
491
|
+
data.setdefault("type", type_string)
|
492
|
+
super().__init__(**data)
|
493
|
+
|
494
|
+
def __new__(cls: Type[Self], **kwargs) -> Self:
|
495
|
+
if "type" in kwargs:
|
496
|
+
try:
|
497
|
+
subcls = lookup_type(cls, dispatch_key=kwargs["type"])
|
498
|
+
except KeyError as exc:
|
499
|
+
raise pydantic.ValidationError(errors=[exc], model=cls)
|
500
|
+
return super().__new__(subcls)
|
501
|
+
else:
|
502
|
+
return super().__new__(cls)
|
503
|
+
|
489
504
|
_cache: Any = pydantic.PrivateAttr(NotSet)
|
490
505
|
|
491
506
|
def _cache_object(self, obj: Any) -> None:
|
@@ -511,6 +526,10 @@ class BaseResult(pydantic.BaseModel, abc.ABC, Generic[R]):
|
|
511
526
|
class Config:
|
512
527
|
extra = "forbid"
|
513
528
|
|
529
|
+
@classmethod
|
530
|
+
def __dispatch_key__(cls, **kwargs):
|
531
|
+
return cls.__fields__.get("type").get_default()
|
532
|
+
|
514
533
|
|
515
534
|
class UnpersistedResult(BaseResult):
|
516
535
|
"""
|
@@ -713,7 +732,7 @@ class PersistedResultBlob(pydantic.BaseModel):
|
|
713
732
|
|
714
733
|
class UnknownResult(BaseResult):
|
715
734
|
"""
|
716
|
-
Result type for unknown results.
|
735
|
+
Result type for unknown results. Typically used to represent the result
|
717
736
|
of tasks that were forced from a failure state into a completed state.
|
718
737
|
|
719
738
|
The value for this result is always None and is not persisted to external
|