schemathesis 3.35.3__py3-none-any.whl → 3.35.5__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 (81) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/cli/__init__.py +19 -13
  6. schemathesis/cli/callbacks.py +6 -4
  7. schemathesis/cli/cassettes.py +67 -41
  8. schemathesis/cli/context.py +7 -6
  9. schemathesis/cli/junitxml.py +1 -1
  10. schemathesis/cli/options.py +7 -4
  11. schemathesis/cli/output/default.py +5 -5
  12. schemathesis/cli/reporting.py +4 -2
  13. schemathesis/code_samples.py +4 -3
  14. schemathesis/exceptions.py +4 -3
  15. schemathesis/extra/_flask.py +4 -1
  16. schemathesis/extra/pytest_plugin.py +6 -3
  17. schemathesis/failures.py +2 -1
  18. schemathesis/filters.py +2 -2
  19. schemathesis/generation/__init__.py +2 -2
  20. schemathesis/generation/_hypothesis.py +1 -1
  21. schemathesis/generation/coverage.py +5 -5
  22. schemathesis/graphql.py +0 -1
  23. schemathesis/hooks.py +3 -3
  24. schemathesis/lazy.py +10 -7
  25. schemathesis/loaders.py +3 -3
  26. schemathesis/models.py +39 -15
  27. schemathesis/runner/__init__.py +5 -5
  28. schemathesis/runner/events.py +1 -1
  29. schemathesis/runner/impl/context.py +58 -0
  30. schemathesis/runner/impl/core.py +54 -61
  31. schemathesis/runner/impl/solo.py +17 -20
  32. schemathesis/runner/impl/threadpool.py +65 -71
  33. schemathesis/runner/serialization.py +4 -3
  34. schemathesis/sanitization.py +2 -1
  35. schemathesis/schemas.py +18 -20
  36. schemathesis/serializers.py +2 -0
  37. schemathesis/service/client.py +1 -1
  38. schemathesis/service/events.py +4 -1
  39. schemathesis/service/extensions.py +2 -2
  40. schemathesis/service/hosts.py +4 -2
  41. schemathesis/service/models.py +3 -3
  42. schemathesis/service/report.py +3 -3
  43. schemathesis/service/serialization.py +4 -2
  44. schemathesis/specs/graphql/loaders.py +4 -3
  45. schemathesis/specs/graphql/schemas.py +4 -3
  46. schemathesis/specs/openapi/definitions.py +1 -5
  47. schemathesis/specs/openapi/examples.py +92 -2
  48. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  49. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  50. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  51. schemathesis/specs/openapi/links.py +4 -4
  52. schemathesis/specs/openapi/loaders.py +5 -4
  53. schemathesis/specs/openapi/negative/__init__.py +5 -3
  54. schemathesis/specs/openapi/negative/mutations.py +5 -4
  55. schemathesis/specs/openapi/parameters.py +11 -8
  56. schemathesis/specs/openapi/schemas.py +9 -10
  57. schemathesis/specs/openapi/security.py +6 -4
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  59. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  60. schemathesis/specs/openapi/stateful/types.py +3 -2
  61. schemathesis/stateful/__init__.py +3 -3
  62. schemathesis/stateful/config.py +1 -1
  63. schemathesis/stateful/context.py +3 -3
  64. schemathesis/stateful/events.py +3 -3
  65. schemathesis/stateful/runner.py +5 -4
  66. schemathesis/stateful/sink.py +1 -1
  67. schemathesis/stateful/state_machine.py +5 -5
  68. schemathesis/stateful/statistic.py +3 -1
  69. schemathesis/stateful/validation.py +1 -1
  70. schemathesis/transports/__init__.py +2 -2
  71. schemathesis/transports/asgi.py +7 -0
  72. schemathesis/transports/auth.py +2 -1
  73. schemathesis/transports/content_types.py +1 -1
  74. schemathesis/transports/responses.py +2 -1
  75. schemathesis/utils.py +4 -2
  76. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/METADATA +1 -1
  77. schemathesis-3.35.5.dist-info/RECORD +156 -0
  78. schemathesis-3.35.3.dist-info/RECORD +0 -154
  79. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
  80. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
  81. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from flask import Flask
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from . import _server
6
6
 
7
+ if TYPE_CHECKING:
8
+ from flask import Flask
9
+
7
10
 
8
11
  def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
9
12
  """Start a thread with the given aiohttp application."""
@@ -3,12 +3,11 @@ from __future__ import annotations
3
3
  import unittest
4
4
  from contextlib import contextmanager
5
5
  from functools import partial
6
- from typing import Any, Callable, Generator, Type, cast
6
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
7
7
 
8
8
  import pytest
9
9
  from _pytest import fixtures, nodes
10
10
  from _pytest.config import hookimpl
11
- from _pytest.fixtures import FuncFixtureInfo
12
11
  from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
13
12
  from hypothesis import reporting
14
13
  from hypothesis.errors import InvalidArgument, Unsatisfiable
@@ -30,7 +29,6 @@ from ..exceptions import (
30
29
  UsageError,
31
30
  )
32
31
  from ..internal.result import Ok, Result
33
- from ..models import APIOperation
34
32
  from ..utils import (
35
33
  PARAMETRIZE_MARKER,
36
34
  fail_on_no_matches,
@@ -42,6 +40,11 @@ from ..utils import (
42
40
  validate_given_args,
43
41
  )
44
42
 
43
+ if TYPE_CHECKING:
44
+ from _pytest.fixtures import FuncFixtureInfo
45
+
46
+ from ..models import APIOperation
47
+
45
48
 
46
49
  class SchemathesisFunction(Function):
47
50
  def __init__(
schemathesis/failures.py CHANGED
@@ -2,12 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import textwrap
4
4
  from dataclasses import dataclass
5
- from json import JSONDecodeError
6
5
  from typing import TYPE_CHECKING, Any
7
6
 
8
7
  from schemathesis.internal.output import OutputConfig
9
8
 
10
9
  if TYPE_CHECKING:
10
+ from json import JSONDecodeError
11
+
11
12
  from graphql.error import GraphQLFormattedError
12
13
  from jsonschema import ValidationError
13
14
 
schemathesis/filters.py CHANGED
@@ -54,7 +54,7 @@ class Matcher:
54
54
  func = partial(by_value_list, attribute=attribute, expected=expected)
55
55
  else:
56
56
  func = partial(by_value, attribute=attribute, expected=expected)
57
- label = f"{attribute}={repr(expected)}"
57
+ label = f"{attribute}={expected!r}"
58
58
  return cls(func, label=label, _hash=hash(label))
59
59
 
60
60
  @classmethod
@@ -68,7 +68,7 @@ class Matcher:
68
68
  flags = 0
69
69
  regex = re.compile(regex, flags=flags)
70
70
  func = partial(by_regex, attribute=attribute, regex=regex)
71
- label = f"{attribute}_regex={repr(regex)}"
71
+ label = f"{attribute}_regex={regex!r}"
72
72
  return cls(func, label=label, _hash=hash(label))
73
73
 
74
74
  def match(self, ctx: HasAPIOperation) -> bool:
@@ -4,8 +4,8 @@ import random
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from ._hypothesis import add_single_example, combine_strategies, get_single_example # noqa: E402
8
- from ._methods import DataGenerationMethod, DataGenerationMethodInput # noqa: E402
7
+ from ._hypothesis import add_single_example, combine_strategies, get_single_example
8
+ from ._methods import DataGenerationMethod, DataGenerationMethodInput
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from hypothesis.strategies import SearchStrategy
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
  from hypothesis import strategies as st
10
10
 
11
11
 
12
- @lru_cache()
12
+ @lru_cache
13
13
  def default_settings() -> settings:
14
14
  from hypothesis import HealthCheck, Phase, Verbosity, settings
15
15
 
@@ -4,7 +4,7 @@ import json
4
4
  from contextlib import contextmanager, suppress
5
5
  from dataclasses import dataclass, field
6
6
  from functools import lru_cache
7
- from typing import Any, Generator, Set, Type, TypeVar, cast
7
+ from typing import Any, Generator, TypeVar, cast
8
8
 
9
9
  import jsonschema
10
10
  from hypothesis import strategies as st
@@ -140,8 +140,8 @@ def _ignore_unfixable(
140
140
  *,
141
141
  # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
142
142
  # and it may cause errors during the interpreter shutdown
143
- ref_error: Type[Exception] = jsonschema.RefResolutionError,
144
- schema_error: Type[Exception] = jsonschema.SchemaError,
143
+ ref_error: type[Exception] = jsonschema.RefResolutionError,
144
+ schema_error: type[Exception] = jsonschema.SchemaError,
145
145
  ) -> Generator:
146
146
  try:
147
147
  yield
@@ -172,7 +172,7 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
172
172
  yield from _cover_positive_for_type(ctx, schema, ty)
173
173
  if DataGenerationMethod.negative in ctx.data_generation_methods:
174
174
  template = None
175
- seen: Set[Any | tuple[type, str]] = set()
175
+ seen: set[Any | tuple[type, str]] = set()
176
176
  for key, value in schema.items():
177
177
  with _ignore_unfixable():
178
178
  if key == "enum":
@@ -238,7 +238,7 @@ def _get_properties(schema: dict | bool) -> dict | bool:
238
238
  if isinstance(schema, dict):
239
239
  if "example" in schema:
240
240
  return {"const": schema["example"]}
241
- if "examples" in schema and schema["examples"]:
241
+ if schema.get("examples"):
242
242
  return {"enum": schema["examples"]}
243
243
  if schema.get("type") == "object":
244
244
  return _get_template_schema(schema, "object")
schemathesis/graphql.py CHANGED
@@ -1,4 +1,3 @@
1
- # Public API
2
1
  from .specs.graphql import nodes # noqa: F401
3
2
  from .specs.graphql.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi # noqa: F401
4
3
  from .specs.graphql.scalars import scalar # noqa: F401
schemathesis/hooks.py CHANGED
@@ -6,11 +6,10 @@ from copy import deepcopy
6
6
  from dataclasses import dataclass, field
7
7
  from enum import Enum, unique
8
8
  from functools import partial
9
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
9
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
10
10
 
11
11
  from .filters import FilterSet, attach_filter_chain
12
12
  from .internal.deprecation import deprecated_property
13
- from .types import GenericTest
14
13
 
15
14
  if TYPE_CHECKING:
16
15
  from hypothesis import strategies as st
@@ -18,6 +17,7 @@ if TYPE_CHECKING:
18
17
  from .models import APIOperation, Case
19
18
  from .schemas import BaseSchema
20
19
  from .transports.responses import GenericResponse
20
+ from .types import GenericTest
21
21
 
22
22
 
23
23
  @unique
@@ -108,7 +108,7 @@ class HookDispatcher:
108
108
  """
109
109
 
110
110
  scope: HookScope
111
- _hooks: DefaultDict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
111
+ _hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
112
112
  _specs: ClassVar[dict[str, RegisteredHook]] = {}
113
113
 
114
114
  def __post_init__(self) -> None:
schemathesis/lazy.py CHANGED
@@ -2,15 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from inspect import signature
5
- from typing import Any, Callable, Generator
5
+ from typing import TYPE_CHECKING, Any, Callable, Generator
6
6
 
7
7
  import pytest
8
- from _pytest.fixtures import FixtureRequest
9
8
  from hypothesis.core import HypothesisHandle
10
9
  from hypothesis.errors import Flaky
11
10
  from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
12
11
  from hypothesis.internal.reflection import impersonate
13
- from pyrate_limiter import Limiter
14
12
  from pytest_subtests import SubTests, nullcontext
15
13
 
16
14
  from ._compat import MultipleFailures, get_interesting_origin
@@ -20,14 +18,10 @@ from .code_samples import CodeSampleStyle
20
18
  from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
21
19
  from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
22
20
  from .filters import FilterSet, FilterValue, MatcherFunc, RegexValue, filter_set_from_components, is_deprecated
23
- from .generation import DataGenerationMethodInput, GenerationConfig
24
21
  from .hooks import HookDispatcher, HookScope
25
22
  from .internal.deprecation import warn_filtration_arguments
26
- from .internal.output import OutputConfig
27
23
  from .internal.result import Ok
28
- from .models import APIOperation
29
24
  from .schemas import BaseSchema
30
- from .types import Filter, GenericTest, NotSet
31
25
  from .utils import (
32
26
  GivenInput,
33
27
  fail_on_no_matches,
@@ -39,6 +33,15 @@ from .utils import (
39
33
  validate_given_args,
40
34
  )
41
35
 
36
+ if TYPE_CHECKING:
37
+ from _pytest.fixtures import FixtureRequest
38
+ from pyrate_limiter import Limiter
39
+
40
+ from .generation import DataGenerationMethodInput, GenerationConfig
41
+ from .internal.output import OutputConfig
42
+ from .models import APIOperation
43
+ from .types import Filter, GenericTest, NotSet
44
+
42
45
 
43
46
  @dataclass
44
47
  class LazySchema:
schemathesis/loaders.py CHANGED
@@ -51,13 +51,13 @@ def _raise_for_status(response: GenericResponse) -> None:
51
51
  else:
52
52
  type_ = SchemaErrorType.HTTP_CLIENT_ERROR
53
53
  else:
54
- return None
54
+ return
55
55
  raise SchemaError(message=message, type=type_, url=response.request.url, response=response, extras=[])
56
56
 
57
57
 
58
58
  def load_app(path: str) -> Any:
59
59
  """Import an application from a string."""
60
- path, name = (re.split(r":(?![\\/])", path, maxsplit=1) + [""])[:2]
60
+ path, name = ([*re.split(":(?![\\\\/])", path, maxsplit=1), ""])[:2]
61
61
  __import__(path)
62
62
  # accessing the module from sys.modules returns a proper module, while `__import__`
63
63
  # may return a parent module (system dependent)
@@ -92,7 +92,7 @@ def get_yaml_loader() -> type[yaml.SafeLoader]:
92
92
  |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
93
93
  |[-+]?\.(?:inf|Inf|INF)
94
94
  |\.(?:nan|NaN|NAN))$""",
95
- re.X,
95
+ re.VERBOSE,
96
96
  ),
97
97
  list("-+0123456789."),
98
98
  )
schemathesis/models.py CHANGED
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
9
9
  from enum import Enum
10
10
  from functools import lru_cache, partial
11
11
  from itertools import chain
12
- from logging import LogRecord
13
12
  from typing import (
14
13
  TYPE_CHECKING,
15
14
  Any,
@@ -28,7 +27,6 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
28
27
 
29
28
  from . import serializers
30
29
  from ._dependency_versions import IS_WERKZEUG_ABOVE_3
31
- from .auths import AuthStorage
32
30
  from .code_samples import CodeSampleStyle
33
31
  from .constants import (
34
32
  NOT_SET,
@@ -38,7 +36,6 @@ from .constants import (
38
36
  )
39
37
  from .exceptions import (
40
38
  CheckFailed,
41
- FailureContext,
42
39
  OperationSchemaError,
43
40
  SerializationNotPossible,
44
41
  SkipTest,
@@ -54,19 +51,22 @@ from .internal.deprecation import deprecated_function, deprecated_property
54
51
  from .internal.output import prepare_response_payload
55
52
  from .parameters import Parameter, ParameterSet, PayloadAlternatives
56
53
  from .sanitization import sanitize_request, sanitize_response
57
- from .serializers import Serializer
58
54
  from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
59
55
  from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
60
56
 
61
57
  if TYPE_CHECKING:
62
58
  import unittest
59
+ from logging import LogRecord
63
60
 
64
61
  import requests.auth
65
62
  import werkzeug
66
63
  from hypothesis import strategies as st
67
64
  from requests.structures import CaseInsensitiveDict
68
65
 
66
+ from .auths import AuthStorage
67
+ from .failures import FailureContext
69
68
  from .schemas import BaseSchema
69
+ from .serializers import Serializer
70
70
  from .stateful import Stateful, StatefulTest
71
71
  from .transports.responses import GenericResponse, WSGIResponse
72
72
 
@@ -995,7 +995,7 @@ class Interaction:
995
995
  """A single interaction with the target app."""
996
996
 
997
997
  request: Request
998
- response: Response
998
+ response: Response | None
999
999
  checks: list[Check]
1000
1000
  status: Status
1001
1001
  data_generation_method: DataGenerationMethod
@@ -1003,10 +1003,28 @@ class Interaction:
1003
1003
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
1004
1004
 
1005
1005
  @classmethod
1006
- def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
1006
+ def from_requests(
1007
+ cls,
1008
+ case: Case,
1009
+ response: requests.Response | None,
1010
+ status: Status,
1011
+ checks: list[Check],
1012
+ headers: dict[str, Any] | None,
1013
+ session: requests.Session | None,
1014
+ ) -> Interaction:
1015
+ if response is not None:
1016
+ prepared = response.request
1017
+ request = Request.from_prepared_request(prepared)
1018
+ else:
1019
+ import requests
1020
+
1021
+ if session is None:
1022
+ session = requests.Session()
1023
+ session.headers.update(headers or {})
1024
+ request = Request.from_case(case, session)
1007
1025
  return cls(
1008
- request=Request.from_prepared_request(response.request),
1009
- response=Response.from_requests(response),
1026
+ request=request,
1027
+ response=Response.from_requests(response) if response is not None else None,
1010
1028
  status=status,
1011
1029
  checks=checks,
1012
1030
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
@@ -1017,9 +1035,9 @@ class Interaction:
1017
1035
  def from_wsgi(
1018
1036
  cls,
1019
1037
  case: Case,
1020
- response: WSGIResponse,
1038
+ response: WSGIResponse | None,
1021
1039
  headers: dict[str, Any],
1022
- elapsed: float,
1040
+ elapsed: float | None,
1023
1041
  status: Status,
1024
1042
  checks: list[Check],
1025
1043
  ) -> Interaction:
@@ -1029,7 +1047,7 @@ class Interaction:
1029
1047
  session.headers.update(headers)
1030
1048
  return cls(
1031
1049
  request=Request.from_case(case, session),
1032
- response=Response.from_wsgi(response, elapsed),
1050
+ response=Response.from_wsgi(response, elapsed) if response is not None and elapsed is not None else None,
1033
1051
  status=status,
1034
1052
  checks=checks,
1035
1053
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
@@ -1119,16 +1137,22 @@ class TestResult:
1119
1137
  self.errors.append(exception)
1120
1138
 
1121
1139
  def store_requests_response(
1122
- self, case: Case, response: requests.Response, status: Status, checks: list[Check]
1140
+ self,
1141
+ case: Case,
1142
+ response: requests.Response | None,
1143
+ status: Status,
1144
+ checks: list[Check],
1145
+ headers: dict[str, Any] | None,
1146
+ session: requests.Session | None,
1123
1147
  ) -> None:
1124
- self.interactions.append(Interaction.from_requests(case, response, status, checks))
1148
+ self.interactions.append(Interaction.from_requests(case, response, status, checks, headers, session))
1125
1149
 
1126
1150
  def store_wsgi_response(
1127
1151
  self,
1128
1152
  case: Case,
1129
- response: WSGIResponse,
1153
+ response: WSGIResponse | None,
1130
1154
  headers: dict[str, Any],
1131
- elapsed: float,
1155
+ elapsed: float | None,
1132
1156
  status: Status,
1133
1157
  checks: list[Check],
1134
1158
  ) -> None:
@@ -4,7 +4,6 @@ from random import Random
4
4
  from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
5
5
  from urllib.parse import urlparse
6
6
 
7
- from .._override import CaseOverride
8
7
  from ..constants import (
9
8
  DEFAULT_DEADLINE,
10
9
  DEFAULT_STATEFUL_RECURSION_LIMIT,
@@ -22,16 +21,17 @@ from ..targets import DEFAULT_TARGETS, Target
22
21
  from ..transports import RequestConfig
23
22
  from ..transports.auth import get_requests_auth
24
23
  from ..types import Filter, NotSet, RawAuth, RequestCert
24
+ from . import events
25
25
  from .probes import ProbeConfig
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  import hypothesis
29
29
 
30
+ from .._override import CaseOverride
30
31
  from ..models import CheckFunction
31
32
  from ..schemas import BaseSchema
32
33
  from ..service.client import ServiceClient
33
34
  from ..stateful import Stateful
34
- from . import events
35
35
  from .impl import BaseRunner
36
36
 
37
37
 
@@ -357,9 +357,9 @@ def from_schema(
357
357
  service_client: ServiceClient | None = None,
358
358
  ) -> BaseRunner:
359
359
  import hypothesis
360
- from starlette.applications import Starlette
361
360
 
362
361
  from ..checks import DEFAULT_CHECKS
362
+ from ..transports.asgi import is_asgi_app
363
363
  from .impl import (
364
364
  SingleThreadASGIRunner,
365
365
  SingleThreadRunner,
@@ -414,7 +414,7 @@ def from_schema(
414
414
  probe_config=probe_config,
415
415
  service_client=service_client,
416
416
  )
417
- if isinstance(schema.app, Starlette):
417
+ if is_asgi_app(schema.app):
418
418
  return ThreadPoolASGIRunner(
419
419
  schema=schema,
420
420
  checks=checks,
@@ -490,7 +490,7 @@ def from_schema(
490
490
  probe_config=probe_config,
491
491
  service_client=service_client,
492
492
  )
493
- if isinstance(schema.app, Starlette):
493
+ if is_asgi_app(schema.app):
494
494
  return SingleThreadASGIRunner(
495
495
  schema=schema,
496
496
  checks=checks,
@@ -7,12 +7,12 @@ from dataclasses import asdict, dataclass, field
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
10
- from ..generation import DataGenerationMethod
11
10
  from ..internal.datetime import current_datetime
12
11
  from ..internal.result import Err, Ok, Result
13
12
  from .serialization import SerializedError, SerializedTestResult
14
13
 
15
14
  if TYPE_CHECKING:
15
+ from ..generation import DataGenerationMethod
16
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
17
17
  from ..schemas import BaseSchema, Specification
18
18
  from ..service.models import AnalysisResult
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ...models import TestResult, TestResultSet
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ...exceptions import OperationSchemaError
10
+ import threading
11
+
12
+
13
+ @dataclass
14
+ class RunnerContext:
15
+ """Holds context shared for a test run."""
16
+
17
+ data: TestResultSet
18
+ seed: int | None
19
+ stop_event: threading.Event
20
+
21
+ __slots__ = ("data", "seed", "stop_event")
22
+
23
+ def __init__(self, seed: int | None, stop_event: threading.Event) -> None:
24
+ self.data = TestResultSet(seed=seed)
25
+ self.seed = seed
26
+ self.stop_event = stop_event
27
+
28
+ @property
29
+ def is_stopped(self) -> bool:
30
+ return self.stop_event.is_set()
31
+
32
+ @property
33
+ def has_all_not_found(self) -> bool:
34
+ """Check if all responses are 404."""
35
+ has_not_found = False
36
+ for entry in self.data.results:
37
+ for check in entry.checks:
38
+ if check.response is not None:
39
+ if check.response.status_code == 404:
40
+ has_not_found = True
41
+ else:
42
+ # There are non-404 responses, no reason to check any other response
43
+ return False
44
+ # Only happens if all responses are 404, or there are no responses at all.
45
+ # In the first case, it returns True, for the latter - False
46
+ return has_not_found
47
+
48
+ def add_result(self, result: TestResult) -> None:
49
+ self.data.append(result)
50
+
51
+ def add_generic_error(self, error: OperationSchemaError) -> None:
52
+ self.data.generic_errors.append(error)
53
+
54
+ def add_warning(self, message: str) -> None:
55
+ self.data.add_warning(message)
56
+
57
+
58
+ ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"