schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -2,14 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from dataclasses import dataclass
5
- from functools import cached_property
6
5
  from typing import TYPE_CHECKING, Any
7
6
 
8
- from schemathesis.checks import CheckContext
7
+ from schemathesis.config import ProjectConfig
9
8
  from schemathesis.core import NOT_SET, NotSet
10
- from schemathesis.engine.recorder import ScenarioRecorder
11
9
  from schemathesis.generation.case import Case
12
- from schemathesis.schemas import BaseSchema
10
+ from schemathesis.schemas import APIOperation, BaseSchema
13
11
 
14
12
  from .control import ExecutionControl
15
13
 
@@ -18,8 +16,6 @@ if TYPE_CHECKING:
18
16
 
19
17
  import requests
20
18
 
21
- from schemathesis.engine.config import EngineConfig
22
-
23
19
 
24
20
  @dataclass
25
21
  class EngineContext:
@@ -28,26 +24,30 @@ class EngineContext:
28
24
  schema: BaseSchema
29
25
  control: ExecutionControl
30
26
  outcome_cache: dict[int, BaseException | None]
31
- config: EngineConfig
32
27
  start_time: float
33
28
 
29
+ __slots__ = ("schema", "control", "outcome_cache", "start_time", "_session", "_transport_kwargs_cache")
30
+
34
31
  def __init__(
35
32
  self,
36
33
  *,
37
34
  schema: BaseSchema,
38
35
  stop_event: threading.Event,
39
- config: EngineConfig,
40
36
  session: requests.Session | None = None,
41
37
  ) -> None:
42
38
  self.schema = schema
43
- self.control = ExecutionControl(stop_event=stop_event, max_failures=config.execution.max_failures)
39
+ self.control = ExecutionControl(stop_event=stop_event, max_failures=schema.config.max_failures)
44
40
  self.outcome_cache = {}
45
- self.config = config
46
41
  self.start_time = time.monotonic()
47
42
  self._session = session
43
+ self._transport_kwargs_cache: dict[str | None, dict[str, Any]] = {}
48
44
 
49
45
  def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
50
46
 
47
+ @property
48
+ def config(self) -> ProjectConfig:
49
+ return self.schema.config
50
+
51
51
  @property
52
52
  def running_time(self) -> float:
53
53
  return time.monotonic() - self.start_time
@@ -74,46 +74,45 @@ class EngineContext:
74
74
  def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
75
75
  return self.outcome_cache.get(hash(case), NOT_SET)
76
76
 
77
- @cached_property
78
- def session(self) -> requests.Session:
77
+ def get_session(self, *, operation: APIOperation | None = None) -> requests.Session:
79
78
  if self._session is not None:
80
79
  return self._session
81
80
  import requests
82
81
 
83
82
  session = requests.Session()
84
- config = self.config.network
85
- session.verify = config.tls_verify
86
- if config.auth is not None:
87
- session.auth = config.auth
88
- if config.headers:
89
- session.headers.update(config.headers)
90
- if config.cert is not None:
91
- session.cert = config.cert
92
- if config.proxy is not None:
93
- session.proxies["all"] = config.proxy
83
+ session.headers = {}
84
+ config = self.config
85
+
86
+ session.verify = config.tls_verify_for(operation=operation)
87
+ auth = config.auth_for(operation=operation)
88
+ if auth is not None:
89
+ session.auth = auth
90
+ headers = config.headers_for(operation=operation)
91
+ if headers:
92
+ session.headers.update(headers)
93
+ request_cert = config.request_cert_for(operation=operation)
94
+ if request_cert is not None:
95
+ session.cert = request_cert
96
+ proxy = config.proxy_for(operation=operation)
97
+ if proxy is not None:
98
+ session.proxies["all"] = proxy
94
99
  return session
95
100
 
96
- @property
97
- def transport_kwargs(self) -> dict[str, Any]:
101
+ def get_transport_kwargs(self, operation: APIOperation | None = None) -> dict[str, Any]:
102
+ key = operation.label if operation is not None else None
103
+ cached = self._transport_kwargs_cache.get(key)
104
+ if cached is not None:
105
+ return cached.copy()
106
+ config = self.config
98
107
  kwargs: dict[str, Any] = {
99
- "session": self.session,
100
- "headers": self.config.network.headers,
101
- "timeout": self.config.network.timeout,
102
- "verify": self.config.network.tls_verify,
103
- "cert": self.config.network.cert,
108
+ "session": self.get_session(operation=operation),
109
+ "headers": config.headers_for(operation=operation),
110
+ "timeout": config.request_timeout_for(operation=operation),
111
+ "verify": config.tls_verify_for(operation=operation),
112
+ "cert": config.request_cert_for(operation=operation),
104
113
  }
105
- if self.config.network.proxy is not None:
106
- kwargs["proxies"] = {"all": self.config.network.proxy}
114
+ proxy = config.proxy_for(operation=operation)
115
+ if proxy is not None:
116
+ kwargs["proxies"] = {"all": proxy}
117
+ self._transport_kwargs_cache[key] = kwargs
107
118
  return kwargs
108
-
109
- def get_check_context(self, recorder: ScenarioRecorder) -> CheckContext:
110
- from requests.models import CaseInsensitiveDict
111
-
112
- return CheckContext(
113
- override=self.config.override,
114
- auth=self.config.network.auth,
115
- headers=CaseInsensitiveDict(self.config.network.headers) if self.config.network.headers else None,
116
- config=self.config.checks_config,
117
- transport_kwargs=self.transport_kwargs,
118
- recorder=recorder,
119
- )
@@ -9,7 +9,6 @@ from schemathesis.core import SpecificationFeature
9
9
  from schemathesis.engine import Status, events, phases
10
10
  from schemathesis.schemas import BaseSchema
11
11
 
12
- from .config import EngineConfig
13
12
  from .context import EngineContext
14
13
  from .events import EventGenerator
15
14
  from .phases import Phase, PhaseName, PhaseSkipReason
@@ -18,15 +17,14 @@ from .phases import Phase, PhaseName, PhaseSkipReason
18
17
  @dataclass
19
18
  class Engine:
20
19
  schema: BaseSchema
21
- config: EngineConfig
22
20
 
23
21
  def execute(self) -> EventStream:
24
22
  """Execute all test phases."""
25
23
  # Unregister auth if explicitly provided
26
- if self.config.network.auth is not None:
24
+ if self.schema.config.auth.is_defined:
27
25
  unregister_auth()
28
26
 
29
- ctx = EngineContext(schema=self.schema, stop_event=threading.Event(), config=self.config)
27
+ ctx = EngineContext(schema=self.schema, stop_event=threading.Event())
30
28
  plan = self._create_execution_plan()
31
29
  return EventStream(plan.execute(ctx), ctx.control.stop_event)
32
30
 
@@ -70,7 +68,11 @@ class Engine:
70
68
  skip_reason=PhaseSkipReason.NOT_SUPPORTED,
71
69
  )
72
70
 
73
- if phase_name not in self.config.execution.phases:
71
+ phase = phase_name.value.lower()
72
+ if (
73
+ phase in ("examples", "coverage", "fuzzing", "stateful")
74
+ and not self.schema.config.phases.get_by_name(name=phase).enabled
75
+ ):
74
76
  return Phase(
75
77
  name=phase_name,
76
78
  is_supported=True,
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import enum
10
10
  import re
11
+ from dataclasses import dataclass
11
12
  from functools import cached_property
12
13
  from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
13
14
 
@@ -24,6 +25,8 @@ from schemathesis.core.errors import (
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  import hypothesis.errors
28
+ import requests
29
+ from requests.exceptions import ChunkedEncodingError
27
30
 
28
31
  __all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnsupportedRecursiveReference", "UnexpectedError"]
29
32
 
@@ -61,8 +64,9 @@ class EngineErrorInfo:
61
64
  It serves as a caching wrapper around exceptions to avoid repeated computations.
62
65
  """
63
66
 
64
- def __init__(self, error: Exception) -> None:
67
+ def __init__(self, error: Exception, code_sample: str | None = None) -> None:
65
68
  self._error = error
69
+ self._code_sample = code_sample
66
70
 
67
71
  def __str__(self) -> str:
68
72
  return self._error_repr
@@ -212,6 +216,9 @@ class EngineErrorInfo:
212
216
  message.append("") # Empty line before extras
213
217
  message.extend(f"{indent}{extra}" for extra in extras)
214
218
 
219
+ if self._code_sample is not None:
220
+ message.append(f"\nReproduce with: \n\n {self._code_sample}")
221
+
215
222
  # Suggestion
216
223
  suggestion = get_runtime_error_suggestion(self._kind, bold=bold)
217
224
  if suggestion is not None:
@@ -403,3 +410,55 @@ def canonicalize_error_message(error: Exception, with_traceback: bool = True) ->
403
410
  message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
404
411
  # Remove URL information
405
412
  return URL_IN_ERROR_MESSAGE_RE.sub("", message)
413
+
414
+
415
+ def clear_hypothesis_notes(exc: Exception) -> None:
416
+ notes = getattr(exc, "__notes__", [])
417
+ if any("while generating" in note for note in notes):
418
+ notes.clear()
419
+
420
+
421
+ def is_unrecoverable_network_error(exc: Exception) -> bool:
422
+ from http.client import RemoteDisconnected
423
+
424
+ from urllib3.exceptions import ProtocolError
425
+
426
+ def has_connection_reset(inner: BaseException) -> bool:
427
+ exc_str = str(inner)
428
+ if any(pattern in exc_str for pattern in ["Connection reset by peer", "[Errno 104]", "ECONNRESET"]):
429
+ return True
430
+
431
+ if inner.__context__ is not None:
432
+ return has_connection_reset(inner.__context__)
433
+
434
+ return False
435
+
436
+ if isinstance(exc.__context__, ProtocolError):
437
+ if len(exc.__context__.args) == 2 and isinstance(exc.__context__.args[1], RemoteDisconnected):
438
+ return True
439
+ if len(exc.__context__.args) == 1 and exc.__context__.args[0] == "Response ended prematurely":
440
+ return True
441
+
442
+ return has_connection_reset(exc)
443
+
444
+
445
+ @dataclass()
446
+ class UnrecoverableNetworkError:
447
+ error: requests.ConnectionError | ChunkedEncodingError
448
+ code_sample: str
449
+
450
+ __slots__ = ("error", "code_sample")
451
+
452
+ def __init__(self, error: requests.ConnectionError | ChunkedEncodingError, code_sample: str) -> None:
453
+ self.error = error
454
+ self.code_sample = code_sample
455
+
456
+
457
+ @dataclass
458
+ class TestingState:
459
+ unrecoverable_network_error: UnrecoverableNetworkError | None
460
+
461
+ __slots__ = ("unrecoverable_network_error",)
462
+
463
+ def __init__(self) -> None:
464
+ self.unrecoverable_network_error = None
@@ -200,10 +200,18 @@ class NonFatalError(EngineEvent):
200
200
 
201
201
  __slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
202
202
 
203
- def __init__(self, *, error: Exception, phase: PhaseName, label: str, related_to_operation: bool) -> None:
203
+ def __init__(
204
+ self,
205
+ *,
206
+ error: Exception,
207
+ phase: PhaseName,
208
+ label: str,
209
+ related_to_operation: bool,
210
+ code_sample: str | None = None,
211
+ ) -> None:
204
212
  self.id = uuid.uuid4()
205
213
  self.timestamp = time.time()
206
- self.info = EngineErrorInfo(error=error)
214
+ self.info = EngineErrorInfo(error=error, code_sample=code_sample)
207
215
  self.value = error
208
216
  self.phase = phase
209
217
  self.label = label
@@ -23,6 +23,16 @@ class PhaseName(str, enum.Enum):
23
23
  def defaults(cls) -> list[PhaseName]:
24
24
  return [PhaseName.EXAMPLES, PhaseName.COVERAGE, PhaseName.FUZZING, PhaseName.STATEFUL_TESTING]
25
25
 
26
+ @property
27
+ def name(self) -> str:
28
+ return {
29
+ PhaseName.PROBING: "probing",
30
+ PhaseName.EXAMPLES: "examples",
31
+ PhaseName.COVERAGE: "coverage",
32
+ PhaseName.FUZZING: "fuzzing",
33
+ PhaseName.STATEFUL_TESTING: "stateful",
34
+ }[self]
35
+
26
36
  @classmethod
27
37
  def from_str(cls, value: str) -> PhaseName:
28
38
  return {
@@ -16,11 +16,11 @@ from typing import TYPE_CHECKING
16
16
  from schemathesis.core.result import Err, Ok, Result
17
17
  from schemathesis.core.transport import USER_AGENT
18
18
  from schemathesis.engine import Status, events
19
+ from schemathesis.transport.prepare import get_default_headers
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  import requests
22
23
 
23
- from schemathesis.engine.config import NetworkConfig
24
24
  from schemathesis.engine.context import EngineContext
25
25
  from schemathesis.engine.events import EventGenerator
26
26
  from schemathesis.engine.phases import Phase
@@ -36,7 +36,7 @@ class ProbePayload:
36
36
 
37
37
  def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
38
38
  """Discover capabilities of the tested app."""
39
- probes = run(ctx.schema, ctx.session, ctx.config.network)
39
+ probes = run(ctx)
40
40
  status = Status.SUCCESS
41
41
  payload: Result[ProbePayload, Exception] | None = None
42
42
  for result in probes:
@@ -44,7 +44,7 @@ def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
44
44
  from ...specs.openapi import formats
45
45
  from ...specs.openapi.formats import HEADER_FORMAT, header_values
46
46
 
47
- formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
47
+ formats.register(HEADER_FORMAT, header_values(exclude_characters="\n\r\x00"))
48
48
  if result.error is not None:
49
49
  status = Status.ERROR
50
50
  payload = Err(result.error)
@@ -54,9 +54,9 @@ def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
54
54
  yield events.PhaseFinished(phase=phase, status=status, payload=payload)
55
55
 
56
56
 
57
- def run(schema: BaseSchema, session: requests.Session, config: NetworkConfig) -> list[ProbeRun]:
57
+ def run(ctx: EngineContext) -> list[ProbeRun]:
58
58
  """Run all probes against the given schema."""
59
- return [send(probe(), session, schema, config) for probe in PROBES]
59
+ return [send(probe(), ctx) for probe in PROBES]
60
60
 
61
61
 
62
62
  HEADER_NAME = "X-Schemathesis-Probe"
@@ -124,19 +124,22 @@ class NullByteInHeader(Probe):
124
124
  PROBES = (NullByteInHeader,)
125
125
 
126
126
 
127
- def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: NetworkConfig) -> ProbeRun:
127
+ def send(probe: Probe, ctx: EngineContext) -> ProbeRun:
128
128
  """Send the probe to the application."""
129
129
  from requests import PreparedRequest, Request, RequestException
130
130
  from requests.exceptions import MissingSchema
131
131
  from urllib3.exceptions import InsecureRequestWarning
132
132
 
133
133
  try:
134
- request = probe.prepare_request(session, Request(), schema)
134
+ session = ctx.get_session()
135
+ request = probe.prepare_request(session, Request(), ctx.schema)
135
136
  request.headers[HEADER_NAME] = probe.name
136
137
  request.headers["User-Agent"] = USER_AGENT
138
+ for header, value in get_default_headers().items():
139
+ request.headers.setdefault(header, value)
137
140
  with warnings.catch_warnings():
138
141
  warnings.simplefilter("ignore", InsecureRequestWarning)
139
- response = session.send(request, timeout=config.timeout or 2)
142
+ response = session.send(request, timeout=ctx.config.request_timeout or 2)
140
143
  except MissingSchema:
141
144
  # In-process ASGI/WSGI testing will have local URLs and requires extra handling
142
145
  # which is not currently implemented
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from schemathesis.engine import Status, events
8
8
  from schemathesis.engine.phases import Phase, PhaseName, PhaseSkipReason
9
+ from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from schemathesis.engine.context import EngineContext
@@ -19,7 +20,7 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
19
20
  try:
20
21
  state_machine = engine.schema.as_state_machine()
21
22
  except Exception as exc:
22
- yield events.NonFatalError(error=exc, phase=phase.name, label="Stateful tests", related_to_operation=False)
23
+ yield events.NonFatalError(error=exc, phase=phase.name, label=STATEFUL_TESTS_LABEL, related_to_operation=False)
23
24
  yield events.PhaseFinished(phase=phase, status=Status.ERROR, payload=None)
24
25
  return
25
26