schemathesis 3.39.15__py3-none-any.whl → 4.0.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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/checks.py CHANGED
@@ -1,80 +1,189 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import TYPE_CHECKING
5
-
6
- from . import failures
7
- from .exceptions import get_response_parsing_error, get_server_error
8
- from .specs.openapi.checks import (
9
- content_type_conformance,
10
- ignored_auth,
11
- negative_data_rejection,
12
- response_headers_conformance,
13
- response_schema_conformance,
14
- status_code_conformance,
4
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
5
+
6
+ from schemathesis.config import ChecksConfig
7
+ from schemathesis.core.failures import (
8
+ CustomFailure,
9
+ Failure,
10
+ FailureGroup,
11
+ MalformedJson,
12
+ ResponseTimeExceeded,
13
+ ServerError,
15
14
  )
15
+ from schemathesis.core.registries import Registry
16
+ from schemathesis.core.transport import Response
17
+ from schemathesis.generation.overrides import Override
16
18
 
17
19
  if TYPE_CHECKING:
18
- from .internal.checks import CheckContext, CheckFunction
19
- from .models import Case
20
- from .transports.responses import GenericResponse
20
+ from requests.models import CaseInsensitiveDict
21
21
 
22
+ from schemathesis.engine.recorder import ScenarioRecorder
23
+ from schemathesis.generation.case import Case
22
24
 
23
- def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
24
- """A check to verify that the response is not a server-side error."""
25
- from .specs.graphql.schemas import GraphQLCase
26
- from .specs.graphql.validation import validate_graphql_response
27
- from .transports.responses import get_json
25
+ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
28
26
 
29
- status_code = response.status_code
30
- if status_code >= 500:
31
- exc_class = get_server_error(case.operation.verbose_name, status_code)
32
- raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
33
- if isinstance(case, GraphQLCase):
34
- try:
35
- data = get_json(response)
36
- validate_graphql_response(data)
37
- except json.JSONDecodeError as exc:
38
- exc_class = get_response_parsing_error(case.operation.verbose_name, exc)
39
- context = failures.JSONDecodeErrorContext.from_exception(exc)
40
- raise exc_class(context.title, context=context) from exc
41
- return None
42
27
 
28
+ class CheckContext:
29
+ """Runtime context passed to validation check functions during API testing.
43
30
 
44
- def _make_max_response_time_failure_message(elapsed_time: float, max_response_time: int) -> str:
45
- return f"Actual: {elapsed_time:.2f}ms\nLimit: {max_response_time}.00ms"
31
+ Provides access to configuration for currently checked endpoint.
32
+ """
46
33
 
34
+ _override: Override | None
35
+ _auth: tuple[str, str] | None
36
+ _headers: CaseInsensitiveDict | None
37
+ config: ChecksConfig
38
+ """Configuration settings for validation checks."""
39
+ _transport_kwargs: dict[str, Any] | None
40
+ _recorder: ScenarioRecorder | None
41
+ _checks: list[CheckFunction]
42
+
43
+ __slots__ = ("_override", "_auth", "_headers", "config", "_transport_kwargs", "_recorder", "_checks")
44
+
45
+ def __init__(
46
+ self,
47
+ override: Override | None,
48
+ auth: tuple[str, str] | None,
49
+ headers: CaseInsensitiveDict | None,
50
+ config: ChecksConfig,
51
+ transport_kwargs: dict[str, Any] | None,
52
+ recorder: ScenarioRecorder | None = None,
53
+ ) -> None:
54
+ self._override = override
55
+ self._auth = auth
56
+ self._headers = headers
57
+ self.config = config
58
+ self._transport_kwargs = transport_kwargs
59
+ self._recorder = recorder
60
+ self._checks = []
61
+ for check in CHECKS.get_all():
62
+ name = check.__name__
63
+ if self.config.get_by_name(name=name).enabled:
64
+ self._checks.append(check)
65
+ if self.config.max_response_time.enabled:
66
+ self._checks.append(max_response_time)
67
+
68
+ def _find_parent(self, *, case_id: str) -> Case | None:
69
+ if self._recorder is not None:
70
+ return self._recorder.find_parent(case_id=case_id)
71
+ return None
72
+
73
+ def _find_related(self, *, case_id: str) -> Iterator[Case]:
74
+ if self._recorder is not None:
75
+ yield from self._recorder.find_related(case_id=case_id)
76
+
77
+ def _find_response(self, *, case_id: str) -> Response | None:
78
+ if self._recorder is not None:
79
+ return self._recorder.find_response(case_id=case_id)
80
+ return None
81
+
82
+ def _record_case(self, *, parent_id: str, case: Case) -> None:
83
+ if self._recorder is not None:
84
+ self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
85
+
86
+ def _record_response(self, *, case_id: str, response: Response) -> None:
87
+ if self._recorder is not None:
88
+ self._recorder.record_response(case_id=case_id, response=response)
89
+
90
+
91
+ CHECKS = Registry[CheckFunction]()
92
+
93
+
94
+ def load_all_checks() -> None:
95
+ # NOTE: Trigger registering all Open API checks
96
+ from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401, F403
97
+
98
+
99
+ def check(func: CheckFunction) -> CheckFunction:
100
+ """Register a custom validation check to run against API responses.
101
+
102
+ Args:
103
+ func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
104
+
105
+ Example:
106
+ ```python
107
+ import schemathesis
47
108
 
48
- DEFAULT_CHECKS: tuple[CheckFunction, ...] = (not_a_server_error,)
49
- OPTIONAL_CHECKS = (
50
- status_code_conformance,
51
- content_type_conformance,
52
- response_headers_conformance,
53
- response_schema_conformance,
54
- negative_data_rejection,
55
- ignored_auth,
56
- )
57
- ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
109
+ @schemathesis.check
110
+ def check_cors_headers(ctx, response, case):
111
+ \"\"\"Verify CORS headers are present\"\"\"
112
+ if "Access-Control-Allow-Origin" not in response.headers:
113
+ raise AssertionError("Missing CORS headers")
114
+ ```
58
115
 
116
+ """
117
+ return CHECKS.register(func)
59
118
 
60
- def register(check: CheckFunction) -> CheckFunction:
61
- """Register a new check for schemathesis CLI.
62
119
 
63
- :param check: A function to validate API responses.
120
+ @check
121
+ def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
122
+ """A check to verify that the response is not a server-side error."""
123
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
124
+ from schemathesis.specs.graphql.validation import validate_graphql_response
125
+ from schemathesis.specs.openapi.utils import expand_status_codes
64
126
 
65
- .. code-block:: python
127
+ expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
128
+
129
+ status_code = response.status_code
130
+ if status_code not in expected_statuses:
131
+ raise ServerError(operation=case.operation.label, status_code=status_code)
132
+ if isinstance(case.operation.schema, GraphQLSchema):
133
+ try:
134
+ data = response.json()
135
+ validate_graphql_response(case, data)
136
+ except json.JSONDecodeError as exc:
137
+ raise MalformedJson.from_exception(operation=case.operation.label, exc=exc) from None
138
+ return None
66
139
 
67
- @schemathesis.check
68
- def new_check(ctx, response, case):
69
- # some awesome assertions!
70
- ...
71
- """
72
- from . import cli
73
- from .internal.checks import wrap_check
74
140
 
75
- _check = wrap_check(check)
76
- global ALL_CHECKS
141
+ DEFAULT_MAX_RESPONSE_TIME = 10.0
77
142
 
78
- ALL_CHECKS += (_check,)
79
- cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
80
- return check
143
+
144
+ def max_response_time(ctx: CheckContext, response: Response, case: Case) -> bool | None:
145
+ limit = ctx.config.max_response_time.limit or DEFAULT_MAX_RESPONSE_TIME
146
+ elapsed = response.elapsed
147
+ if elapsed > limit:
148
+ raise ResponseTimeExceeded(
149
+ operation=case.operation.label,
150
+ message=f"Actual: {elapsed:.2f}ms\nLimit: {limit * 1000:.2f}ms",
151
+ elapsed=elapsed,
152
+ deadline=limit,
153
+ )
154
+ return None
155
+
156
+
157
+ def run_checks(
158
+ *,
159
+ case: Case,
160
+ response: Response,
161
+ ctx: CheckContext,
162
+ checks: Iterable[CheckFunction],
163
+ on_failure: Callable[[str, set[Failure], Failure], None],
164
+ on_success: Callable[[str, Case], None] | None = None,
165
+ ) -> set[Failure]:
166
+ """Run a set of checks against a response."""
167
+ collected: set[Failure] = set()
168
+
169
+ for check in checks:
170
+ name = check.__name__
171
+ try:
172
+ skip_check = check(ctx, response, case)
173
+ if not skip_check and on_success:
174
+ on_success(name, case)
175
+ except Failure as failure:
176
+ on_failure(name, collected, failure.with_traceback(None))
177
+ except AssertionError as exc:
178
+ custom_failure = CustomFailure(
179
+ operation=case.operation.label,
180
+ title=f"Custom check failed: `{name}`",
181
+ message=str(exc),
182
+ exception=exc,
183
+ )
184
+ on_failure(name, collected, custom_failure)
185
+ except FailureGroup as group:
186
+ for sub_failure in group.exceptions:
187
+ on_failure(name, collected, sub_failure)
188
+
189
+ return collected