prefect-client 2.17.0__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.0.dist-info → prefect_client-2.18.0.dist-info}/METADATA +1 -1
  42. {prefect_client-2.17.0.dist-info → prefect_client-2.18.0.dist-info}/RECORD +45 -42
  43. {prefect_client-2.17.0.dist-info → prefect_client-2.18.0.dist-info}/LICENSE +0 -0
  44. {prefect_client-2.17.0.dist-info → prefect_client-2.18.0.dist-info}/WHEEL +0 -0
  45. {prefect_client-2.17.0.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 Automation(PrefectBaseModel, extra="ignore"):
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 ExistingAutomation(Automation):
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
- Automation,
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) -> Automation:
108
+ def as_automation(self) -> AutomationCore:
139
109
  if not self.name:
140
110
  raise ValueError("name is required")
141
111
 
142
- return Automation(
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 EventTrigger(
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 MetricTrigger(
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 CompoundTrigger(
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 SequenceTrigger(
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) -> Automation:
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 Automation(
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(
@@ -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 emit_events_to_cloud() or should_emit_events_to_running_server()
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"][0]}: {err["msg"]}' for err in exc.errors()]
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
 
@@ -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"]
@@ -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 typing import TYPE_CHECKING, Dict, Optional, Union
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 add_type_dispatch
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
- @add_type_dispatch
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. Typipcally used to represent the result
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