schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import lru_cache
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis import settings
9
+ from hypothesis import strategies as st
10
+
11
+ SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
12
+
13
+
14
+ @lru_cache
15
+ def default_settings() -> settings:
16
+ from hypothesis import HealthCheck, Phase, Verbosity, settings
17
+
18
+ return settings(
19
+ database=None,
20
+ max_examples=1,
21
+ deadline=None,
22
+ verbosity=Verbosity.quiet,
23
+ phases=(Phase.generate,),
24
+ suppress_health_check=list(HealthCheck),
25
+ )
26
+
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ def generate_one(strategy: st.SearchStrategy[T], suppress_health_check: list | None = None) -> T: # type: ignore[type-var]
32
+ examples: list[T] = []
33
+ add_single_example(strategy, examples, suppress_health_check)
34
+ return examples[0]
35
+
36
+
37
+ def add_single_example(
38
+ strategy: st.SearchStrategy[T], examples: list[T], suppress_health_check: list | None = None
39
+ ) -> None:
40
+ from hypothesis import given, seed, settings
41
+
42
+ applied_settings = default_settings()
43
+ if suppress_health_check is not None:
44
+ applied_settings = settings(applied_settings, suppress_health_check=suppress_health_check)
45
+
46
+ @given(strategy) # type: ignore[misc]
47
+ @applied_settings # type: ignore[misc]
48
+ def example_generating_inner_function(ex: T) -> None:
49
+ examples.append(ex)
50
+
51
+ example_generating_inner_function._hypothesis_internal_database_key = b""
52
+
53
+ if SCHEMATHESIS_BENCHMARK_SEED is not None:
54
+ example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
55
+
56
+ example_generating_inner_function()
@@ -0,0 +1,66 @@
1
+ """Integrating `hypothesis.given` into Schemathesis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from inspect import getfullargspec
6
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union
7
+
8
+ from schemathesis.core.errors import IncorrectUsage
9
+ from schemathesis.core.marks import Mark
10
+
11
+ if TYPE_CHECKING:
12
+ from hypothesis.strategies import SearchStrategy
13
+
14
+
15
+ __all__ = ["is_given_applied", "given_proxy", "merge_given_args", "GivenInput", "GivenArgsMark", "GivenKwargsMark"]
16
+
17
+ EllipsisType = type(...)
18
+ GivenInput = Union["SearchStrategy", EllipsisType] # type: ignore[valid-type]
19
+
20
+ GivenArgsMark = Mark[tuple](attr_name="given_args", default=())
21
+ GivenKwargsMark = Mark[dict[str, Any]](attr_name="given_kwargs", default=dict)
22
+
23
+
24
+ def is_given_applied(func: Callable) -> bool:
25
+ return GivenArgsMark.is_set(func) or GivenKwargsMark.is_set(func)
26
+
27
+
28
+ def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[Callable], Callable]:
29
+ """Proxy Hypothesis strategies to ``hypothesis.given``."""
30
+
31
+ def wrapper(func: Callable) -> Callable:
32
+ if is_given_applied(func):
33
+
34
+ def wrapped_test(*_: Any, **__: Any) -> NoReturn:
35
+ raise IncorrectUsage(
36
+ f"You have applied `given` to the `{func.__name__}` test more than once, which "
37
+ "overrides the previous decorator. You need to pass all arguments to the same `given` call."
38
+ )
39
+
40
+ return wrapped_test
41
+
42
+ GivenArgsMark.set(func, args)
43
+ GivenKwargsMark.set(func, kwargs)
44
+ return func
45
+
46
+ return wrapper
47
+
48
+
49
+ def merge_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
50
+ """Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
51
+
52
+ Kwargs are modified inplace.
53
+ """
54
+ if args:
55
+ argspec = getfullargspec(func)
56
+ for name, strategy in zip(reversed([arg for arg in argspec.args if arg != "case"]), reversed(args)):
57
+ kwargs[name] = strategy
58
+ return kwargs
59
+
60
+
61
+ def validate_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
62
+ from hypothesis.core import is_invalid_test
63
+ from hypothesis.internal.reflection import get_signature
64
+
65
+ signature = get_signature(func)
66
+ return is_invalid_test(func, signature, args, kwargs)
@@ -0,0 +1,285 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING, Any, Generator
7
+
8
+ from hypothesis import HealthCheck
9
+ from hypothesis.errors import FailedHealthCheck, InvalidArgument, Unsatisfiable
10
+ from hypothesis.reporting import with_reporter
11
+
12
+ from schemathesis.config import OutputConfig
13
+ from schemathesis.core.jsonschema.types import JsonSchema
14
+ from schemathesis.core.output import truncate_json
15
+ from schemathesis.core.parameters import ParameterLocation
16
+ from schemathesis.generation.hypothesis.examples import generate_one
17
+
18
+ if TYPE_CHECKING:
19
+ from schemathesis.schemas import APIOperation
20
+
21
+
22
+ def ignore(_: str) -> None:
23
+ pass
24
+
25
+
26
+ @contextmanager
27
+ def ignore_hypothesis_output() -> Generator:
28
+ with with_reporter(ignore):
29
+ yield
30
+
31
+
32
+ UNSATISFIABILITY_CAUSE = """ - Type mismatch (e.g., enum with strings but type: integer)
33
+ - Contradictory constraints (e.g., minimum > maximum)
34
+ - Regex that's too complex to generate values for"""
35
+
36
+ GENERIC_UNSATISFIABLE_MESSAGE = f"""Cannot generate test data for this operation
37
+
38
+ Unable to identify the specific parameter. Common causes:
39
+ {UNSATISFIABILITY_CAUSE}"""
40
+
41
+
42
+ @dataclass
43
+ class UnsatisfiableParameter:
44
+ location: ParameterLocation
45
+ name: str
46
+ schema: JsonSchema
47
+
48
+ __slots__ = ("location", "name", "schema")
49
+
50
+ def get_error_message(self, config: OutputConfig) -> str:
51
+ formatted_schema = truncate_json(self.schema, config=config)
52
+
53
+ if self.location == ParameterLocation.BODY:
54
+ # For body, name is the media type
55
+ location = f"request body ({self.name})"
56
+ else:
57
+ location = f"{self.location.value} parameter '{self.name}'"
58
+
59
+ return f"""Cannot generate test data for {location}
60
+ Schema:
61
+
62
+ {formatted_schema}
63
+
64
+ This usually means:
65
+ {UNSATISFIABILITY_CAUSE}"""
66
+
67
+
68
+ def find_unsatisfiable_parameter(operation: APIOperation) -> UnsatisfiableParameter | None:
69
+ from hypothesis_jsonschema import from_schema
70
+
71
+ for location, container in (
72
+ (ParameterLocation.QUERY, operation.query),
73
+ (ParameterLocation.PATH, operation.path_parameters),
74
+ (ParameterLocation.HEADER, operation.headers),
75
+ (ParameterLocation.COOKIE, operation.cookies),
76
+ (ParameterLocation.BODY, operation.body),
77
+ ):
78
+ for parameter in container:
79
+ try:
80
+ generate_one(from_schema(parameter.optimized_schema))
81
+ except Unsatisfiable:
82
+ if location == ParameterLocation.BODY:
83
+ name = parameter.media_type
84
+ else:
85
+ name = parameter.name
86
+ schema = unbundle_schema_refs(parameter.optimized_schema, parameter.name_to_uri)
87
+ return UnsatisfiableParameter(location=location, name=name, schema=schema)
88
+ return None
89
+
90
+
91
+ def unbundle_schema_refs(schema: JsonSchema | list[JsonSchema], name_to_uri: dict[str, str]) -> JsonSchema:
92
+ if isinstance(schema, dict):
93
+ result: dict[str, Any] = {}
94
+ for key, value in schema.items():
95
+ if key == "$ref" and isinstance(value, str) and value.startswith("#/x-bundled/"):
96
+ # Extract bundled name (e.g., "schema1" from "#/x-bundled/schema1")
97
+ bundled_name = value.split("/")[-1]
98
+ if bundled_name in name_to_uri:
99
+ original_uri = name_to_uri[bundled_name]
100
+ # Extract fragment after # (e.g., "#/components/schemas/ObjectType")
101
+ if "#" in original_uri:
102
+ result[key] = "#" + original_uri.split("#", 1)[1]
103
+ else:
104
+ # Fallback if no fragment
105
+ result[key] = value
106
+ else:
107
+ result[key] = value
108
+ elif key == "x-bundled" and isinstance(value, dict):
109
+ # Replace x-bundled with proper components/schemas structure
110
+ components: dict[str, dict[str, Any]] = {"schemas": {}}
111
+ for bundled_name, bundled_schema in value.items():
112
+ if bundled_name in name_to_uri:
113
+ original_uri = name_to_uri[bundled_name]
114
+ # Extract schema name (e.g., "ObjectType" from "...#/components/schemas/ObjectType")
115
+ if "#/components/schemas/" in original_uri:
116
+ schema_name = original_uri.split("#/components/schemas/")[1]
117
+ components["schemas"][schema_name] = unbundle_schema_refs(bundled_schema, name_to_uri)
118
+ else:
119
+ # Fallback: keep bundled name if URI doesn't match expected pattern
120
+ components["schemas"][bundled_name] = unbundle_schema_refs(bundled_schema, name_to_uri)
121
+ else:
122
+ components["schemas"][bundled_name] = unbundle_schema_refs(bundled_schema, name_to_uri)
123
+ result["components"] = components
124
+ elif isinstance(value, (dict, list)):
125
+ # Recursively process all other values
126
+ result[key] = unbundle_schema_refs(value, name_to_uri)
127
+ else:
128
+ result[key] = value
129
+ return result
130
+ elif isinstance(schema, list):
131
+ return [unbundle_schema_refs(item, name_to_uri) for item in schema] # type: ignore[return-value]
132
+ return schema
133
+
134
+
135
+ def build_unsatisfiable_error(operation: APIOperation, *, with_tip: bool) -> Unsatisfiable:
136
+ __tracebackhide__ = True
137
+ unsatisfiable = find_unsatisfiable_parameter(operation)
138
+
139
+ if unsatisfiable is not None:
140
+ message = unsatisfiable.get_error_message(operation.schema.config.output)
141
+ else:
142
+ message = GENERIC_UNSATISFIABLE_MESSAGE
143
+
144
+ if with_tip:
145
+ message += "\n\nTip: Review all parameters and request body schemas for conflicting constraints"
146
+
147
+ return Unsatisfiable(message)
148
+
149
+
150
+ HEALTH_CHECK_CAUSES = {
151
+ HealthCheck.data_too_large: """ - Arrays with large minItems (e.g., minItems: 1000)
152
+ - Strings with large minLength (e.g., minLength: 10000)
153
+ - Deeply nested objects with many required properties""",
154
+ HealthCheck.filter_too_much: """ - Complex regex patterns that match few strings
155
+ - Multiple overlapping constraints (pattern + format + enum)""",
156
+ HealthCheck.too_slow: """ - Regex with excessive backtracking (e.g., (a+)+b)
157
+ - Many interdependent constraints
158
+ - Large combinatorial complexity""",
159
+ HealthCheck.large_base_example: """ - Arrays with large minimum size (e.g., minItems: 100)
160
+ - Many required properties with their own large minimums
161
+ - Nested structures that multiply size requirements""",
162
+ }
163
+
164
+ HEALTH_CHECK_ACTIONS = {
165
+ HealthCheck.data_too_large: "Reduce minItems, minLength, or size constraints to realistic values",
166
+ HealthCheck.filter_too_much: "Simplify constraints or widen acceptable value ranges",
167
+ HealthCheck.too_slow: "Simplify regex patterns or reduce constraint complexity",
168
+ HealthCheck.large_base_example: "Reduce minimum size requirements or number of required properties",
169
+ }
170
+
171
+ HEALTH_CHECK_TITLES = {
172
+ HealthCheck.data_too_large: "Generated examples exceed size limits",
173
+ HealthCheck.filter_too_much: "Too many generated examples are filtered out",
174
+ HealthCheck.too_slow: "Data generation is too slow",
175
+ HealthCheck.large_base_example: "Minimum possible example is too large",
176
+ }
177
+
178
+
179
+ @dataclass
180
+ class SlowParameter:
181
+ """Information about a parameter with slow or problematic data generation."""
182
+
183
+ location: ParameterLocation
184
+ name: str
185
+ schema: JsonSchema
186
+ original: HealthCheck
187
+
188
+ __slots__ = ("location", "name", "schema", "original")
189
+
190
+ def get_error_message(self, config: OutputConfig) -> str:
191
+ formatted_schema = truncate_json(self.schema, config=config)
192
+ if self.location == ParameterLocation.BODY:
193
+ # For body, name is the media type
194
+ location = f"request body ({self.name})"
195
+ else:
196
+ location = f"{self.location.value} parameter '{self.name}'"
197
+ title = HEALTH_CHECK_TITLES[self.original]
198
+ causes = HEALTH_CHECK_CAUSES[self.original]
199
+
200
+ return f"""{title} for {location}
201
+ Schema:
202
+
203
+ {formatted_schema}
204
+
205
+ This usually means:
206
+ {causes}"""
207
+
208
+
209
+ def _extract_health_check_reason(exc: FailedHealthCheck | InvalidArgument) -> HealthCheck | None:
210
+ message = str(exc).lower()
211
+ if "data_too_large" in message or "too large" in message:
212
+ return HealthCheck.data_too_large
213
+ elif "filter_too_much" in message or "filtered out" in message:
214
+ return HealthCheck.filter_too_much
215
+ elif "too_slow" in message or "too slow" in message:
216
+ return HealthCheck.too_slow
217
+ elif ("large_base_example" in message or "can never generate an example, because min_size" in message) or (
218
+ isinstance(exc, InvalidArgument)
219
+ and message.endswith("larger than hypothesis is designed to handle")
220
+ or "can never generate an example, because min_size is larger than hypothesis supports" in message
221
+ ):
222
+ return HealthCheck.large_base_example
223
+
224
+ return None
225
+
226
+
227
+ def find_slow_parameter(operation: APIOperation, reason: HealthCheck) -> SlowParameter | None:
228
+ from hypothesis.errors import FailedHealthCheck
229
+ from hypothesis_jsonschema import from_schema
230
+
231
+ for location, container in (
232
+ (ParameterLocation.QUERY, operation.query),
233
+ (ParameterLocation.PATH, operation.path_parameters),
234
+ (ParameterLocation.HEADER, operation.headers),
235
+ (ParameterLocation.COOKIE, operation.cookies),
236
+ (ParameterLocation.BODY, operation.body),
237
+ ):
238
+ for parameter in container:
239
+ try:
240
+ generate_one(from_schema(parameter.optimized_schema), suppress_health_check=[])
241
+ except (FailedHealthCheck, Unsatisfiable, InvalidArgument):
242
+ if location == ParameterLocation.BODY:
243
+ name = parameter.media_type
244
+ else:
245
+ name = parameter.name
246
+
247
+ schema = unbundle_schema_refs(parameter.optimized_schema, parameter.name_to_uri)
248
+ return SlowParameter(location=location, name=name, schema=schema, original=reason)
249
+ return None
250
+
251
+
252
+ def _get_generic_health_check_message(reason: HealthCheck) -> str:
253
+ title = HEALTH_CHECK_TITLES[reason]
254
+ causes = HEALTH_CHECK_CAUSES[reason]
255
+ return f"{title} for this operation\n\nUnable to identify the specific parameter. Common causes:\n{causes}"
256
+
257
+
258
+ class HealthCheckTipStyle(Enum):
259
+ DEFAULT = "default"
260
+ PYTEST = "pytest"
261
+
262
+
263
+ def build_health_check_error(
264
+ operation: APIOperation,
265
+ original: FailedHealthCheck | InvalidArgument,
266
+ with_tip: bool,
267
+ tip_style: HealthCheckTipStyle = HealthCheckTipStyle.DEFAULT,
268
+ ) -> FailedHealthCheck | InvalidArgument:
269
+ __tracebackhide__ = True
270
+ reason = _extract_health_check_reason(original)
271
+ if reason is None:
272
+ return original
273
+ slow_param = find_slow_parameter(operation, reason)
274
+
275
+ if slow_param is not None:
276
+ message = slow_param.get_error_message(operation.schema.config.output)
277
+ else:
278
+ message = _get_generic_health_check_message(reason)
279
+
280
+ if with_tip:
281
+ message += f"\n\nTip: {HEALTH_CHECK_ACTIONS[reason]}"
282
+ if tip_style == HealthCheckTipStyle.PYTEST:
283
+ message += f". You can disable this health check with @settings(suppress_health_check=[{reason!r}])"
284
+
285
+ return FailedHealthCheck(message)
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from schemathesis.core.parameters import ParameterLocation
7
+ from schemathesis.generation import GenerationMode
8
+
9
+
10
+ class TestPhase(str, Enum):
11
+ __test__ = False
12
+
13
+ EXAMPLES = "examples"
14
+ COVERAGE = "coverage"
15
+ FUZZING = "fuzzing"
16
+ STATEFUL = "stateful"
17
+
18
+
19
+ class CoverageScenario(str, Enum):
20
+ """Coverage test scenario types."""
21
+
22
+ # Positive scenarios - Valid values
23
+ EXAMPLE_VALUE = "example_value"
24
+ DEFAULT_VALUE = "default_value"
25
+ ENUM_VALUE = "enum_value"
26
+ CONST_VALUE = "const_value"
27
+ VALID_STRING = "valid_string"
28
+ VALID_NUMBER = "valid_number"
29
+ VALID_BOOLEAN = "valid_boolean"
30
+ VALID_ARRAY = "valid_array"
31
+ VALID_OBJECT = "valid_object"
32
+ NULL_VALUE = "null_value"
33
+
34
+ # Positive scenarios - Boundary values for strings
35
+ MINIMUM_LENGTH_STRING = "minimum_length_string"
36
+ MAXIMUM_LENGTH_STRING = "maximum_length_string"
37
+ NEAR_BOUNDARY_LENGTH_STRING = "near_boundary_length_string"
38
+
39
+ # Positive scenarios - Boundary values for numbers
40
+ MINIMUM_VALUE = "minimum_value"
41
+ MAXIMUM_VALUE = "maximum_value"
42
+ NEAR_BOUNDARY_NUMBER = "near_boundary_number"
43
+
44
+ # Positive scenarios - Boundary values for arrays
45
+ MINIMUM_ITEMS_ARRAY = "minimum_items_array"
46
+ MAXIMUM_ITEMS_ARRAY = "maximum_items_array"
47
+ NEAR_BOUNDARY_ITEMS_ARRAY = "near_boundary_items_array"
48
+ ENUM_VALUE_ITEMS_ARRAY = "enum_value_items_array"
49
+
50
+ # Positive scenarios - Objects
51
+ OBJECT_ONLY_REQUIRED = "object_only_required"
52
+ OBJECT_REQUIRED_AND_OPTIONAL = "object_required_and_optional"
53
+
54
+ # Positive scenarios - Default test case
55
+ DEFAULT_POSITIVE_TEST = "default_positive_test"
56
+
57
+ # Negative scenarios - Boundary violations for numbers
58
+ VALUE_ABOVE_MAXIMUM = "value_above_maximum"
59
+ VALUE_BELOW_MINIMUM = "value_below_minimum"
60
+
61
+ # Negative scenarios - Boundary violations for strings
62
+ STRING_ABOVE_MAX_LENGTH = "string_above_max_length"
63
+ STRING_BELOW_MIN_LENGTH = "string_below_min_length"
64
+
65
+ # Negative scenarios - Boundary violations for arrays
66
+ ARRAY_ABOVE_MAX_ITEMS = "array_above_max_items"
67
+ ARRAY_BELOW_MIN_ITEMS = "array_below_min_items"
68
+
69
+ # Negative scenarios - Constraint violations
70
+ OBJECT_UNEXPECTED_PROPERTIES = "object_unexpected_properties"
71
+ OBJECT_MISSING_REQUIRED_PROPERTY = "object_missing_required_property"
72
+ INCORRECT_TYPE = "incorrect_type"
73
+ INVALID_ENUM_VALUE = "invalid_enum_value"
74
+ INVALID_FORMAT = "invalid_format"
75
+ INVALID_PATTERN = "invalid_pattern"
76
+ NOT_MULTIPLE_OF = "not_multiple_of"
77
+ NON_UNIQUE_ITEMS = "non_unique_items"
78
+
79
+ # Negative scenarios - Missing parameters
80
+ MISSING_PARAMETER = "missing_parameter"
81
+ DUPLICATE_PARAMETER = "duplicate_parameter"
82
+
83
+ # Negative scenarios - Unsupported patterns
84
+ UNSUPPORTED_PATH_PATTERN = "unsupported_path_pattern"
85
+ UNSPECIFIED_HTTP_METHOD = "unspecified_http_method"
86
+
87
+
88
+ @dataclass
89
+ class ComponentInfo:
90
+ """Information about how a specific component was generated."""
91
+
92
+ mode: GenerationMode
93
+
94
+ __slots__ = ("mode",)
95
+
96
+
97
+ @dataclass
98
+ class FuzzingPhaseData:
99
+ """Metadata specific to fuzzing phase."""
100
+
101
+ description: str
102
+ parameter: str | None
103
+ parameter_location: ParameterLocation | None
104
+ location: str | None
105
+
106
+ __slots__ = ("description", "parameter", "parameter_location", "location")
107
+
108
+
109
+ @dataclass
110
+ class StatefulPhaseData:
111
+ """Metadata specific to stateful phase."""
112
+
113
+ description: str | None
114
+ parameter: str | None
115
+ parameter_location: ParameterLocation | None
116
+ location: str | None
117
+
118
+ __slots__ = ("description", "parameter", "parameter_location", "location")
119
+
120
+
121
+ @dataclass
122
+ class ExamplesPhaseData:
123
+ """Metadata specific to examples phase."""
124
+
125
+ description: str | None
126
+ parameter: str | None
127
+ parameter_location: ParameterLocation | None
128
+ location: str | None
129
+
130
+ __slots__ = ("description", "parameter", "parameter_location", "location")
131
+
132
+
133
+ @dataclass
134
+ class CoveragePhaseData:
135
+ """Metadata specific to coverage phase."""
136
+
137
+ scenario: CoverageScenario
138
+ description: str
139
+ location: str | None
140
+ parameter: str | None
141
+ parameter_location: ParameterLocation | None
142
+
143
+ __slots__ = ("scenario", "description", "location", "parameter", "parameter_location")
144
+
145
+
146
+ @dataclass
147
+ class PhaseInfo:
148
+ """Phase-specific information."""
149
+
150
+ name: TestPhase
151
+ data: CoveragePhaseData | ExamplesPhaseData | FuzzingPhaseData | StatefulPhaseData
152
+
153
+ __slots__ = ("name", "data")
154
+
155
+ @classmethod
156
+ def coverage(
157
+ cls,
158
+ scenario: CoverageScenario,
159
+ description: str,
160
+ location: str | None = None,
161
+ parameter: str | None = None,
162
+ parameter_location: ParameterLocation | None = None,
163
+ ) -> PhaseInfo:
164
+ return cls(
165
+ name=TestPhase.COVERAGE,
166
+ data=CoveragePhaseData(
167
+ scenario=scenario,
168
+ description=description,
169
+ location=location,
170
+ parameter=parameter,
171
+ parameter_location=parameter_location,
172
+ ),
173
+ )
174
+
175
+
176
+ @dataclass
177
+ class GenerationInfo:
178
+ """Information about test case generation."""
179
+
180
+ time: float
181
+ mode: GenerationMode
182
+
183
+ __slots__ = ("time", "mode")
184
+
185
+
186
+ @dataclass
187
+ class CaseMetadata:
188
+ """Complete metadata for generated cases."""
189
+
190
+ generation: GenerationInfo
191
+ components: dict[ParameterLocation, ComponentInfo]
192
+ phase: PhaseInfo
193
+
194
+ # Dirty tracking for revalidation
195
+ _dirty: set[ParameterLocation]
196
+ _last_validated_hashes: dict[ParameterLocation, int]
197
+
198
+ __slots__ = ("generation", "components", "phase", "_dirty", "_last_validated_hashes")
199
+
200
+ def __init__(
201
+ self,
202
+ generation: GenerationInfo,
203
+ components: dict[ParameterLocation, ComponentInfo],
204
+ phase: PhaseInfo,
205
+ ) -> None:
206
+ self.generation = generation
207
+ self.components = components
208
+ self.phase = phase
209
+ # Initialize dirty tracking
210
+ self._dirty = set()
211
+ self._last_validated_hashes = {}
212
+
213
+ def mark_dirty(self, location: ParameterLocation) -> None:
214
+ """Mark a component as modified and needing revalidation."""
215
+ self._dirty.add(location)
216
+
217
+ def clear_dirty(self, location: ParameterLocation) -> None:
218
+ """Clear dirty flag for a component after revalidation."""
219
+ self._dirty.discard(location)
220
+
221
+ def is_dirty(self) -> bool:
222
+ """Check if any component needs revalidation."""
223
+ return len(self._dirty) > 0
224
+
225
+ def update_validated_hash(self, location: ParameterLocation, value: int) -> None:
226
+ """Store hash after validation to detect future changes."""
227
+ self._last_validated_hashes[location] = value