schemathesis 3.29.1__py3-none-any.whl → 3.30.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 (123) hide show
  1. schemathesis/__init__.py +3 -3
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +1 -3
  4. schemathesis/_hypothesis.py +6 -0
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +1 -0
  7. schemathesis/_rate_limiter.py +2 -1
  8. schemathesis/_xml.py +1 -0
  9. schemathesis/auths.py +4 -2
  10. schemathesis/checks.py +8 -5
  11. schemathesis/cli/__init__.py +8 -1
  12. schemathesis/cli/callbacks.py +3 -4
  13. schemathesis/cli/cassettes.py +6 -4
  14. schemathesis/cli/constants.py +2 -0
  15. schemathesis/cli/context.py +3 -0
  16. schemathesis/cli/debug.py +2 -1
  17. schemathesis/cli/handlers.py +1 -1
  18. schemathesis/cli/options.py +1 -0
  19. schemathesis/cli/output/default.py +50 -22
  20. schemathesis/cli/output/short.py +21 -10
  21. schemathesis/cli/sanitization.py +1 -0
  22. schemathesis/code_samples.py +1 -0
  23. schemathesis/constants.py +1 -0
  24. schemathesis/contrib/openapi/__init__.py +1 -1
  25. schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
  26. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  27. schemathesis/contrib/unique_data.py +2 -1
  28. schemathesis/exceptions.py +40 -26
  29. schemathesis/experimental/__init__.py +14 -0
  30. schemathesis/extra/_aiohttp.py +1 -0
  31. schemathesis/extra/_server.py +1 -0
  32. schemathesis/extra/pytest_plugin.py +13 -24
  33. schemathesis/failures.py +32 -3
  34. schemathesis/filters.py +2 -1
  35. schemathesis/fixups/__init__.py +1 -0
  36. schemathesis/fixups/fast_api.py +2 -2
  37. schemathesis/fixups/utf8_bom.py +1 -2
  38. schemathesis/generation/__init__.py +2 -1
  39. schemathesis/hooks.py +3 -1
  40. schemathesis/internal/copy.py +19 -3
  41. schemathesis/internal/deprecation.py +1 -1
  42. schemathesis/internal/jsonschema.py +2 -1
  43. schemathesis/internal/result.py +1 -1
  44. schemathesis/internal/transformation.py +1 -0
  45. schemathesis/lazy.py +3 -2
  46. schemathesis/loaders.py +4 -2
  47. schemathesis/models.py +20 -5
  48. schemathesis/parameters.py +1 -0
  49. schemathesis/runner/__init__.py +1 -1
  50. schemathesis/runner/events.py +21 -4
  51. schemathesis/runner/impl/core.py +61 -33
  52. schemathesis/runner/impl/solo.py +2 -1
  53. schemathesis/runner/impl/threadpool.py +4 -0
  54. schemathesis/runner/probes.py +1 -1
  55. schemathesis/runner/serialization.py +1 -1
  56. schemathesis/sanitization.py +2 -0
  57. schemathesis/schemas.py +1 -4
  58. schemathesis/service/ci.py +1 -0
  59. schemathesis/service/client.py +7 -7
  60. schemathesis/service/events.py +2 -1
  61. schemathesis/service/extensions.py +5 -5
  62. schemathesis/service/hosts.py +1 -0
  63. schemathesis/service/metadata.py +2 -1
  64. schemathesis/service/models.py +2 -1
  65. schemathesis/service/report.py +3 -3
  66. schemathesis/service/serialization.py +54 -23
  67. schemathesis/service/usage.py +1 -0
  68. schemathesis/specs/graphql/_cache.py +1 -1
  69. schemathesis/specs/graphql/loaders.py +1 -1
  70. schemathesis/specs/graphql/nodes.py +1 -0
  71. schemathesis/specs/graphql/scalars.py +2 -2
  72. schemathesis/specs/graphql/schemas.py +7 -7
  73. schemathesis/specs/graphql/validation.py +1 -2
  74. schemathesis/specs/openapi/_hypothesis.py +17 -11
  75. schemathesis/specs/openapi/checks.py +102 -9
  76. schemathesis/specs/openapi/converter.py +2 -1
  77. schemathesis/specs/openapi/definitions.py +2 -1
  78. schemathesis/specs/openapi/examples.py +7 -9
  79. schemathesis/specs/openapi/expressions/__init__.py +29 -2
  80. schemathesis/specs/openapi/expressions/context.py +1 -1
  81. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  82. schemathesis/specs/openapi/expressions/lexer.py +19 -18
  83. schemathesis/specs/openapi/expressions/nodes.py +24 -4
  84. schemathesis/specs/openapi/expressions/parser.py +26 -5
  85. schemathesis/specs/openapi/filters.py +1 -0
  86. schemathesis/specs/openapi/links.py +35 -7
  87. schemathesis/specs/openapi/loaders.py +13 -11
  88. schemathesis/specs/openapi/negative/__init__.py +2 -1
  89. schemathesis/specs/openapi/negative/mutations.py +1 -0
  90. schemathesis/specs/openapi/parameters.py +1 -0
  91. schemathesis/specs/openapi/schemas.py +27 -38
  92. schemathesis/specs/openapi/security.py +1 -0
  93. schemathesis/specs/openapi/serialization.py +1 -0
  94. schemathesis/specs/openapi/stateful/__init__.py +159 -70
  95. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  96. schemathesis/specs/openapi/stateful/types.py +13 -0
  97. schemathesis/specs/openapi/utils.py +1 -0
  98. schemathesis/specs/openapi/validation.py +1 -0
  99. schemathesis/stateful/__init__.py +4 -2
  100. schemathesis/stateful/config.py +66 -0
  101. schemathesis/stateful/context.py +93 -0
  102. schemathesis/stateful/events.py +209 -0
  103. schemathesis/stateful/runner.py +233 -0
  104. schemathesis/stateful/sink.py +68 -0
  105. schemathesis/stateful/state_machine.py +39 -22
  106. schemathesis/stateful/statistic.py +20 -0
  107. schemathesis/stateful/validation.py +66 -0
  108. schemathesis/targets.py +1 -0
  109. schemathesis/throttling.py +23 -3
  110. schemathesis/transports/__init__.py +28 -10
  111. schemathesis/transports/auth.py +1 -0
  112. schemathesis/transports/content_types.py +1 -1
  113. schemathesis/transports/headers.py +2 -1
  114. schemathesis/transports/responses.py +6 -4
  115. schemathesis/types.py +1 -0
  116. schemathesis/utils.py +1 -0
  117. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/METADATA +1 -1
  118. schemathesis-3.30.0.dist-info/RECORD +150 -0
  119. schemathesis/specs/openapi/stateful/links.py +0 -94
  120. schemathesis-3.29.1.dist-info/RECORD +0 -141
  121. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/WHEEL +0 -0
  122. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/entry_points.txt +0 -0
  123. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
4
 
4
- from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
+ from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
6
  from ._lazy_import import lazy_import
6
- from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
7
7
  from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
+ from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
8
9
  from .models import Case # noqa: E402
9
10
  from .specs import openapi # noqa: E402
10
11
 
11
-
12
12
  __version__ = SCHEMATHESIS_VERSION
13
13
 
14
14
  # Default loaders
schemathesis/_compat.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import Any, Type, Callable
2
- from ._lazy_import import lazy_import
1
+ from typing import Any, Callable, Type
3
2
 
3
+ from ._lazy_import import lazy_import
4
4
 
5
5
  __all__ = [ # noqa: F822
6
6
  "JSONMixin",
@@ -1,16 +1,14 @@
1
1
  """Compatibility flags based on installed dependency versions."""
2
2
 
3
- from packaging import version
4
-
5
3
  from importlib import metadata
6
4
 
5
+ from packaging import version
7
6
 
8
7
  WERKZEUG_VERSION = version.parse(metadata.version("werkzeug"))
9
8
  IS_WERKZEUG_ABOVE_3 = WERKZEUG_VERSION >= version.parse("3.0")
10
9
  IS_WERKZEUG_BELOW_2_1 = WERKZEUG_VERSION < version.parse("2.1.0")
11
10
 
12
11
  PYTEST_VERSION = version.parse(metadata.version("pytest"))
13
- IS_PYTEST_ABOVE_54 = PYTEST_VERSION >= version.parse("5.4.0")
14
12
  IS_PYTEST_ABOVE_7 = PYTEST_VERSION >= version.parse("7.0.0")
15
13
  IS_PYTEST_ABOVE_8 = PYTEST_VERSION >= version.parse("8.0.0")
16
14
 
@@ -10,6 +10,7 @@ import hypothesis
10
10
  from hypothesis import Phase
11
11
  from hypothesis import strategies as st
12
12
  from hypothesis.errors import HypothesisWarning, Unsatisfiable
13
+ from hypothesis.internal.entropy import deterministic_PRNG
13
14
  from hypothesis.internal.reflection import proxies
14
15
  from jsonschema.exceptions import SchemaError
15
16
 
@@ -23,6 +24,11 @@ from .transports.content_types import parse_content_type
23
24
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
24
25
  from .utils import GivenInput, combine_strategies
25
26
 
27
+ # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
28
+ # if e.g. Schemathesis CLI is used with multiple workers
29
+ with deterministic_PRNG():
30
+ pass
31
+
26
32
 
27
33
  def create_test(
28
34
  *,
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any, Callable
3
4
 
4
5
 
schemathesis/_override.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING, Optional
4
5
 
@@ -3,4 +3,5 @@ from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
3
3
  if IS_PYRATE_LIMITER_ABOVE_3:
4
4
  from pyrate_limiter import Limiter, Rate, RateItem
5
5
  else:
6
- from pyrate_limiter import Limiter, RequestRate as Rate
6
+ from pyrate_limiter import Limiter
7
+ from pyrate_limiter import RequestRate as Rate
schemathesis/_xml.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """XML serialization."""
2
2
 
3
3
  from __future__ import annotations
4
+
4
5
  from io import StringIO
5
6
  from typing import Any, Dict, List, Union
6
7
  from xml.etree import ElementTree
schemathesis/auths.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Support for custom API authentication mechanisms."""
2
2
 
3
3
  from __future__ import annotations
4
+
4
5
  import inspect
5
6
  import threading
6
7
  import time
@@ -11,10 +12,10 @@ from typing import (
11
12
  Any,
12
13
  Callable,
13
14
  Generic,
15
+ Protocol,
14
16
  TypeVar,
15
17
  overload,
16
18
  runtime_checkable,
17
- Protocol,
18
19
  )
19
20
 
20
21
  from .exceptions import UsageError
@@ -22,9 +23,10 @@ from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
22
23
  from .types import GenericTest
23
24
 
24
25
  if TYPE_CHECKING:
25
- from .models import APIOperation, Case
26
26
  import requests.auth
27
27
 
28
+ from .models import APIOperation, Case
29
+
28
30
  DEFAULT_REFRESH_INTERVAL = 300
29
31
  AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
30
32
  Auth = TypeVar("Auth")
schemathesis/checks.py CHANGED
@@ -1,19 +1,21 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+
3
3
  import json
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from . import failures
6
- from .exceptions import get_server_error, get_response_parsing_error
7
+ from .exceptions import get_response_parsing_error, get_server_error
7
8
  from .specs.openapi.checks import (
8
9
  content_type_conformance,
10
+ negative_data_rejection,
9
11
  response_headers_conformance,
10
12
  response_schema_conformance,
11
13
  status_code_conformance,
12
14
  )
13
15
 
14
16
  if TYPE_CHECKING:
15
- from .transports.responses import GenericResponse
16
17
  from .models import Case, CheckFunction
18
+ from .transports.responses import GenericResponse
17
19
 
18
20
 
19
21
  def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
@@ -24,14 +26,14 @@ def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
24
26
 
25
27
  status_code = response.status_code
26
28
  if status_code >= 500:
27
- exc_class = get_server_error(status_code)
29
+ exc_class = get_server_error(case.operation.verbose_name, status_code)
28
30
  raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
29
31
  if isinstance(case, GraphQLCase):
30
32
  try:
31
33
  data = get_json(response)
32
34
  validate_graphql_response(data)
33
35
  except json.JSONDecodeError as exc:
34
- exc_class = get_response_parsing_error(exc)
36
+ exc_class = get_response_parsing_error(case.operation.verbose_name, exc)
35
37
  context = failures.JSONDecodeErrorContext.from_exception(exc)
36
38
  raise exc_class(context.title, context=context) from exc
37
39
  return None
@@ -43,6 +45,7 @@ OPTIONAL_CHECKS = (
43
45
  content_type_conformance,
44
46
  response_headers_conformance,
45
47
  response_schema_conformance,
48
+ negative_data_rejection,
46
49
  )
47
50
  ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
48
51
 
@@ -683,7 +683,14 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
683
683
  "--experimental",
684
684
  "experiments",
685
685
  help="Enable experimental support for specific features.",
686
- type=click.Choice([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
686
+ type=click.Choice(
687
+ [
688
+ experimental.OPEN_API_3_1.name,
689
+ experimental.SCHEMA_ANALYSIS.name,
690
+ experimental.STATEFUL_TEST_RUNNER.name,
691
+ experimental.STATEFUL_ONLY.name,
692
+ ]
693
+ ),
687
694
  callback=callbacks.convert_experimental,
688
695
  multiple=True,
689
696
  )
@@ -7,25 +7,24 @@ import re
7
7
  import traceback
8
8
  from contextlib import contextmanager
9
9
  from functools import partial
10
- from typing import Generator, TYPE_CHECKING, Callable
10
+ from typing import TYPE_CHECKING, Callable, Generator
11
11
  from urllib.parse import urlparse
12
12
 
13
13
  import click
14
-
15
14
  from click.types import LazyFile # type: ignore
16
15
 
17
16
  from .. import exceptions, experimental, throttling
18
17
  from ..code_samples import CodeSampleStyle
18
+ from ..constants import FALSE_VALUES, TRUE_VALUES
19
19
  from ..exceptions import extract_nth_traceback
20
20
  from ..generation import DataGenerationMethod
21
- from ..constants import TRUE_VALUES, FALSE_VALUES
22
21
  from ..internal.validation import file_exists, is_filename, is_illegal_surrogate
23
22
  from ..loaders import load_app
24
23
  from ..service.hosts import get_temporary_hosts_file
24
+ from ..stateful import Stateful
25
25
  from ..transports.headers import has_invalid_characters, is_latin_1_encodable
26
26
  from ..types import PathLike
27
27
  from .constants import DEFAULT_WORKERS
28
- from ..stateful import Stateful
29
28
 
30
29
  if TYPE_CHECKING:
31
30
  import hypothesis
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import base64
3
4
  import json
4
5
  import re
@@ -6,7 +7,7 @@ import sys
6
7
  import threading
7
8
  from dataclasses import dataclass, field
8
9
  from queue import Queue
9
- from typing import IO, Any, Generator, Iterator, cast, TYPE_CHECKING
10
+ from typing import IO, TYPE_CHECKING, Any, Generator, Iterator, cast
10
11
 
11
12
  from ..constants import SCHEMATHESIS_VERSION
12
13
  from ..runner import events
@@ -16,10 +17,11 @@ from .handlers import EventHandler
16
17
  if TYPE_CHECKING:
17
18
  import click
18
19
  import requests
20
+
21
+ from ..generation import DataGenerationMethod
19
22
  from ..models import Request, Response
20
23
  from ..runner.serialization import SerializedCheck, SerializedInteraction
21
24
  from .context import ExecutionContext
22
- from ..generation import DataGenerationMethod
23
25
 
24
26
  # Wait until the worker terminates
25
27
  WRITER_WORKER_JOIN_TIMEOUT = 1
@@ -351,9 +353,9 @@ def filter_cassette(
351
353
 
352
354
  def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
353
355
  """Create a `requests.PreparedRequest` from a serialized one."""
354
- from requests.structures import CaseInsensitiveDict
355
- from requests.cookies import RequestsCookieJar
356
356
  import requests
357
+ from requests.cookies import RequestsCookieJar
358
+ from requests.structures import CaseInsensitiveDict
357
359
 
358
360
  prepared = requests.PreparedRequest()
359
361
  prepared.method = data["method"]
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import IntEnum, unique
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  if TYPE_CHECKING:
6
7
  import hypothesis
8
+
7
9
  MIN_WORKERS = 1
8
10
  DEFAULT_WORKERS = MIN_WORKERS
9
11
  MAX_WORKERS = 64
@@ -16,6 +16,8 @@ from ..service.models import AnalysisResult
16
16
  if TYPE_CHECKING:
17
17
  import hypothesis
18
18
 
19
+ from ..stateful.sink import StateMachineSink
20
+
19
21
 
20
22
  @dataclass
21
23
  class ServiceReportContext:
@@ -57,6 +59,7 @@ class ExecutionContext:
57
59
  analysis: Result[AnalysisResult, Exception] | None = None
58
60
  # Special flag to display a warning about Windows-specific encoding issue
59
61
  encountered_windows_encoding_issue: bool = False
62
+ state_machine_sink: StateMachineSink | None = None
60
63
 
61
64
  @deprecated_property(removed_in="4.0", replacement="show_trace")
62
65
  def show_errors_tracebacks(self) -> bool:
schemathesis/cli/debug.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
+
2
3
  import json
3
4
  from dataclasses import dataclass
4
5
  from typing import TYPE_CHECKING
5
6
 
6
-
7
7
  from .handlers import EventHandler
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from click.utils import LazyFile
11
+
11
12
  from ..runner import events
12
13
  from .context import ExecutionContext
13
14
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
3
2
 
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from ..runner import events
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import Enum
3
4
  from typing import Any, NoReturn
4
5
 
@@ -7,11 +7,11 @@ import textwrap
7
7
  import time
8
8
  from importlib import metadata
9
9
  from queue import Queue
10
- from typing import Any, Generator, cast, TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, Any, Generator, Literal, cast
11
11
 
12
12
  import click
13
13
 
14
- from ... import service
14
+ from ... import experimental, service
15
15
  from ...constants import (
16
16
  DISCORD_LINK,
17
17
  FALSE_VALUES,
@@ -24,9 +24,9 @@ from ...constants import (
24
24
  )
25
25
  from ...exceptions import (
26
26
  RuntimeErrorType,
27
+ extract_requests_exception_details,
27
28
  format_exception,
28
29
  prepare_response_payload,
29
- extract_requests_exception_details,
30
30
  )
31
31
  from ...experimental import GLOBAL_EXPERIMENTS
32
32
  from ...internal.result import Ok
@@ -35,10 +35,12 @@ from ...runner import events
35
35
  from ...runner.events import InternalErrorType, SchemaErrorType
36
36
  from ...runner.probes import ProbeOutcome
37
37
  from ...runner.serialization import SerializedError, SerializedTestResult
38
- from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
38
+ from ...service.models import AnalysisSuccess, ErrorState, UnknownExtension
39
+ from ...stateful import events as stateful_events
40
+ from ...stateful.sink import StateMachineSink
39
41
  from ..context import ExecutionContext, FileReportContext, ServiceReportContext
40
42
  from ..handlers import EventHandler
41
- from ..reporting import group_by_case, TEST_CASE_ID_TITLE, split_traceback, get_runtime_error_suggestion
43
+ from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
42
44
 
43
45
  if TYPE_CHECKING:
44
46
  import requests
@@ -68,14 +70,14 @@ def get_percentage(position: int, length: int) -> str:
68
70
  return f"[{percentage_message}]"
69
71
 
70
72
 
71
- def display_execution_result(context: ExecutionContext, event: events.AfterExecution) -> None:
73
+ def display_execution_result(context: ExecutionContext, status: Literal["success", "failure", "error", "skip"]) -> None:
72
74
  """Display an appropriate symbol for the given event's execution result."""
73
75
  symbol, color = {
74
- Status.success: (".", "green"),
75
- Status.failure: ("F", "red"),
76
- Status.error: ("E", "red"),
77
- Status.skip: ("S", "yellow"),
78
- }[event.status]
76
+ "success": (".", "green"),
77
+ "failure": ("F", "red"),
78
+ "error": ("E", "red"),
79
+ "skip": ("S", "yellow"),
80
+ }[status]
79
81
  context.current_line_length += len(symbol)
80
82
  click.secho(symbol, nl=False, fg=color)
81
83
 
@@ -433,6 +435,9 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
433
435
  display_section_name("SUMMARY")
434
436
  click.echo()
435
437
  total = event.total
438
+ if context.state_machine_sink is not None:
439
+ click.echo(context.state_machine_sink.transitions.to_formatted_table(get_terminal_width()))
440
+ click.echo()
436
441
  if event.is_empty or not total:
437
442
  click.secho("No checks were performed.", bold=True)
438
443
 
@@ -551,7 +556,7 @@ def display_report_metadata(meta: service.Metadata) -> None:
551
556
  if value is not None:
552
557
  click.secho(f" -> {key}: {value}")
553
558
  click.echo()
554
- click.secho(f"Compressed report size: {meta.size / 1024.:,.0f} KB", bold=True)
559
+ click.secho(f"Compressed report size: {meta.size / 1024.0:,.0f} KB", bold=True)
555
560
 
556
561
 
557
562
  def display_service_unauthorized(hostname: str) -> None:
@@ -839,7 +844,7 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
839
844
  """Display the execution result + current progress at the same line with the method / path names."""
840
845
  context.operations_processed += 1
841
846
  context.results.append(event.result)
842
- display_execution_result(context, event)
847
+ display_execution_result(context, event.status.value)
843
848
  display_percentage(context, event)
844
849
 
845
850
 
@@ -867,27 +872,50 @@ def handle_internal_error(context: ExecutionContext, event: events.InternalError
867
872
  raise click.Abort
868
873
 
869
874
 
875
+ def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
876
+ if isinstance(event.data, stateful_events.RunStarted):
877
+ context.state_machine_sink = event.data.state_machine.sink()
878
+ if not experimental.STATEFUL_ONLY.is_enabled:
879
+ click.echo()
880
+ click.secho("Stateful tests\n", bold=True)
881
+ elif isinstance(event.data, stateful_events.ScenarioFinished) and not event.data.is_final:
882
+ display_execution_result(context, event.data.status.value)
883
+ elif isinstance(event.data, stateful_events.RunFinished):
884
+ click.echo()
885
+ # It is initialized in `RunStarted`
886
+ sink = cast(StateMachineSink, context.state_machine_sink)
887
+ sink.consume(event.data)
888
+
889
+
890
+ def handle_after_stateful_execution(context: ExecutionContext, event: events.AfterStatefulExecution) -> None:
891
+ context.results.append(event.result)
892
+
893
+
870
894
  class DefaultOutputStyleHandler(EventHandler):
871
895
  def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
872
896
  """Choose and execute a proper handler for the given event."""
873
897
  if isinstance(event, events.Initialized):
874
898
  handle_initialized(context, event)
875
- if isinstance(event, events.BeforeProbing):
899
+ elif isinstance(event, events.BeforeProbing):
876
900
  handle_before_probing(context, event)
877
- if isinstance(event, events.AfterProbing):
901
+ elif isinstance(event, events.AfterProbing):
878
902
  handle_after_probing(context, event)
879
- if isinstance(event, events.BeforeAnalysis):
903
+ elif isinstance(event, events.BeforeAnalysis):
880
904
  handle_before_analysis(context, event)
881
- if isinstance(event, events.AfterAnalysis):
905
+ elif isinstance(event, events.AfterAnalysis):
882
906
  handle_after_analysis(context, event)
883
- if isinstance(event, events.BeforeExecution):
907
+ elif isinstance(event, events.BeforeExecution):
884
908
  handle_before_execution(context, event)
885
- if isinstance(event, events.AfterExecution):
909
+ elif isinstance(event, events.AfterExecution):
886
910
  context.hypothesis_output.extend(event.hypothesis_output)
887
911
  handle_after_execution(context, event)
888
- if isinstance(event, events.Finished):
912
+ elif isinstance(event, events.Finished):
889
913
  handle_finished(context, event)
890
- if isinstance(event, events.Interrupted):
914
+ elif isinstance(event, events.Interrupted):
891
915
  handle_interrupted(context, event)
892
- if isinstance(event, events.InternalError):
916
+ elif isinstance(event, events.InternalError):
893
917
  handle_internal_error(context, event)
918
+ elif isinstance(event, events.StatefulEvent):
919
+ handle_stateful_event(context, event)
920
+ elif isinstance(event, events.AfterStatefulExecution):
921
+ handle_after_stateful_execution(context, event)
@@ -1,6 +1,7 @@
1
1
  import click
2
2
 
3
3
  from ...runner import events
4
+ from ...stateful import events as stateful_events
4
5
  from ..context import ExecutionContext
5
6
  from ..handlers import EventHandler
6
7
  from . import default
@@ -15,7 +16,13 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
15
16
  context.operations_processed += 1
16
17
  context.results.append(event.result)
17
18
  context.hypothesis_output.extend(event.hypothesis_output)
18
- default.display_execution_result(context, event)
19
+ default.display_execution_result(context, event.status.value)
20
+
21
+
22
+ def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
23
+ if isinstance(event.data, stateful_events.RunStarted):
24
+ click.echo()
25
+ default.handle_stateful_event(context, event)
19
26
 
20
27
 
21
28
  class ShortOutputStyleHandler(EventHandler):
@@ -26,23 +33,27 @@ class ShortOutputStyleHandler(EventHandler):
26
33
  """
27
34
  if isinstance(event, events.Initialized):
28
35
  default.handle_initialized(context, event)
29
- if isinstance(event, events.BeforeProbing):
36
+ elif isinstance(event, events.BeforeProbing):
30
37
  default.handle_before_probing(context, event)
31
- if isinstance(event, events.AfterProbing):
38
+ elif isinstance(event, events.AfterProbing):
32
39
  default.handle_after_probing(context, event)
33
- if isinstance(event, events.BeforeAnalysis):
40
+ elif isinstance(event, events.BeforeAnalysis):
34
41
  default.handle_before_analysis(context, event)
35
- if isinstance(event, events.AfterAnalysis):
42
+ elif isinstance(event, events.AfterAnalysis):
36
43
  default.handle_after_analysis(context, event)
37
- if isinstance(event, events.BeforeExecution):
44
+ elif isinstance(event, events.BeforeExecution):
38
45
  handle_before_execution(context, event)
39
- if isinstance(event, events.AfterExecution):
46
+ elif isinstance(event, events.AfterExecution):
40
47
  handle_after_execution(context, event)
41
- if isinstance(event, events.Finished):
48
+ elif isinstance(event, events.Finished):
42
49
  if context.operations_count == context.operations_processed:
43
50
  click.echo()
44
51
  default.handle_finished(context, event)
45
- if isinstance(event, events.Interrupted):
52
+ elif isinstance(event, events.Interrupted):
46
53
  default.handle_interrupted(context, event)
47
- if isinstance(event, events.InternalError):
54
+ elif isinstance(event, events.InternalError):
48
55
  default.handle_internal_error(context, event)
56
+ elif isinstance(event, events.StatefulEvent):
57
+ handle_stateful_event(context, event)
58
+ elif isinstance(event, events.AfterStatefulExecution):
59
+ default.handle_after_stateful_execution(context, event)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING
4
5
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import Enum
3
4
  from functools import lru_cache
4
5
  from shlex import quote
schemathesis/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from importlib import metadata
2
+
2
3
  from .types import NotSet
3
4
 
4
5
  try:
@@ -1,4 +1,4 @@
1
- from . import formats, fill_missing_examples
1
+ from . import fill_missing_examples, formats
2
2
 
3
3
 
4
4
  def install() -> None:
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
4
+
3
5
  from ...hooks import HookContext, register, unregister
4
6
 
5
7
  if TYPE_CHECKING:
@@ -3,9 +3,10 @@ FORMAT_NAME = "uuid"
3
3
 
4
4
 
5
5
  def install() -> None:
6
- from ....specs import openapi
7
6
  from hypothesis import strategies as st
8
7
 
8
+ from ....specs import openapi
9
+
9
10
  openapi.format(FORMAT_NAME, st.uuids().map(str))
10
11
 
11
12
 
@@ -6,9 +6,10 @@ from typing import TYPE_CHECKING
6
6
  from ..hooks import HookContext, register, unregister
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..models import Case
10
9
  from hypothesis import strategies as st
11
10
 
11
+ from ..models import Case
12
+
12
13
 
13
14
  def install() -> None:
14
15
  warnings.warn(