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,101 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, fields, is_dataclass
4
+ from typing import TypeVar
5
+
6
+ T = TypeVar("T", bound="DiffBase")
7
+
8
+
9
+ @dataclass
10
+ class DiffBase:
11
+ def __repr__(self) -> str:
12
+ """Show only the fields that differ from the default."""
13
+ assert is_dataclass(self)
14
+ default = self.__class__()
15
+ diffs = []
16
+ for field in fields(self):
17
+ name = field.name
18
+ if name.startswith("_") and name not in ("_seed", "_filter_set"):
19
+ continue
20
+ current_value = getattr(self, name)
21
+ default_value = getattr(default, name)
22
+ if name == "_seed":
23
+ name = "seed"
24
+ if name == "_filter_set":
25
+ name = "filter_set"
26
+ if name == "rate_limit" and current_value is not None:
27
+ assert hasattr(self, "_rate_limit")
28
+ current_value = self._rate_limit
29
+ if self._has_diff(current_value, default_value):
30
+ diffs.append(f"{name}={self._diff_repr(current_value, default_value)}")
31
+ return f"{self.__class__.__name__}({', '.join(diffs)})"
32
+
33
+ def _has_diff(self, value: object, default: object) -> bool:
34
+ if is_dataclass(value):
35
+ return repr(value) != repr(default)
36
+ if isinstance(value, list) and isinstance(default, list):
37
+ if len(value) != len(default):
38
+ return True
39
+ return any(self._has_diff(v, d) for v, d in zip(value, default))
40
+ if isinstance(value, dict) and isinstance(default, dict):
41
+ if set(value.keys()) != set(default.keys()):
42
+ return True
43
+ return any(self._has_diff(value[k], default[k]) for k in value)
44
+ return value != default
45
+
46
+ def _diff_repr(self, value: object, default: object) -> str:
47
+ if is_dataclass(value):
48
+ # If the nested object is a dataclass, recursively show its diff.
49
+ return repr(value)
50
+ if isinstance(value, list) and isinstance(default, list):
51
+ diff_items = []
52
+ # Compare items pairwise.
53
+ for v, d in zip(value, default):
54
+ if self._has_diff(v, d):
55
+ diff_items.append(self._diff_repr(v, d))
56
+ # Include any extra items in value.
57
+ if len(value) > len(default):
58
+ diff_items.extend(_repr(item) for item in value[len(default) :])
59
+ return f"[{', '.join(_repr(item) for item in value)}]"
60
+ if isinstance(value, dict) and isinstance(default, dict):
61
+ diff_items = []
62
+ for k, v in value.items():
63
+ d = default.get(k)
64
+ if self._has_diff(v, d):
65
+ diff_items.append(f"{k!r}: {self._diff_repr(v, d)}")
66
+ return f"{{{', '.join(diff_items)}}}"
67
+ return repr(value)
68
+
69
+ @classmethod
70
+ def from_hierarchy(cls, configs: list[T]) -> T:
71
+ # This config will accumulate "merged" config options
72
+ if len(configs) == 1:
73
+ return configs[0]
74
+ output = cls()
75
+ for option in cls.__slots__: # type: ignore[attr-defined]
76
+ if option.startswith("_"):
77
+ continue
78
+ default = getattr(output, option)
79
+ if hasattr(default, "__dataclass_fields__"):
80
+ # Sub-configs require merging of nested config options
81
+ sub_configs = [getattr(config, option) for config in configs]
82
+ merged = type(default).from_hierarchy(sub_configs)
83
+ setattr(output, option, merged)
84
+ else:
85
+ # Primitive config options can be compared directly and do not
86
+ # require merging of nested options
87
+ for config in configs:
88
+ current = getattr(config, option)
89
+ if current != default:
90
+ setattr(output, option, current)
91
+ # As we go from the highest priority to the lowest one,
92
+ # we can just stop on the first non-default value
93
+ break
94
+ return output # type: ignore[return-value]
95
+
96
+
97
+ def _repr(item: object) -> str:
98
+ if callable(item) and hasattr(item, "__name__"):
99
+ return f"<function {item.__name__}>"
100
+
101
+ return repr(item)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from string import Template
5
+ from typing import Any
6
+
7
+ from schemathesis.config._error import ConfigError
8
+
9
+
10
+ def resolve(value: Any) -> Any:
11
+ """Resolve environment variables using string templates."""
12
+ if value is None:
13
+ return None
14
+ if not isinstance(value, str):
15
+ return value
16
+ try:
17
+ return Template(value).substitute(os.environ)
18
+ except ValueError:
19
+ raise ConfigError(f"Invalid placeholder in string: `{value}`") from None
20
+ except KeyError:
21
+ raise ConfigError(f"Missing environment variable: `{value}`") from None
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from typing import TYPE_CHECKING
5
+
6
+ from schemathesis.core.errors import SchemathesisError
7
+
8
+ if TYPE_CHECKING:
9
+ from jsonschema import ValidationError
10
+
11
+
12
+ class ConfigError(SchemathesisError):
13
+ """Invalid configuration."""
14
+
15
+ @classmethod
16
+ def from_validation_error(cls, error: ValidationError) -> ConfigError:
17
+ message = error.message
18
+ if error.validator == "enum":
19
+ message = _format_enum_error(error)
20
+ elif error.validator == "minimum":
21
+ message = _format_minimum_error(error)
22
+ elif error.validator == "required":
23
+ message = _format_required_error(error)
24
+ elif error.validator == "type":
25
+ message = _format_type_error(error)
26
+ elif error.validator == "additionalProperties":
27
+ message = _format_additional_properties_error(error)
28
+ elif error.validator == "anyOf":
29
+ message = _format_anyof_error(error)
30
+ return cls(message)
31
+
32
+
33
+ def _format_minimum_error(error: ValidationError) -> str:
34
+ assert isinstance(error.validator_value, (int, float))
35
+ section = path_to_section_name(list(error.path)[:-1] if error.path else [])
36
+ assert error.path
37
+
38
+ prop_name = error.path[-1]
39
+ min_value = error.validator_value
40
+ actual_value = error.instance
41
+
42
+ return (
43
+ f"Error in {section} section:\n Value too low:\n\n"
44
+ f" - '{prop_name}' → Must be at least {min_value}, but got {actual_value}."
45
+ )
46
+
47
+
48
+ def _format_required_error(error: ValidationError) -> str:
49
+ assert isinstance(error.validator_value, list)
50
+ missing_keys = sorted(set(error.validator_value) - set(error.instance))
51
+
52
+ section = path_to_section_name(list(error.path))
53
+
54
+ details = "\n".join(f" - '{key}'" for key in missing_keys)
55
+ return f"Error in {section} section:\n Missing required properties:\n\n{details}\n\n"
56
+
57
+
58
+ def _format_enum_error(error: ValidationError) -> str:
59
+ assert isinstance(error.validator_value, list)
60
+ valid_values = sorted(error.validator_value)
61
+
62
+ path = list(error.path)
63
+
64
+ if path and isinstance(path[-1], int):
65
+ idx = path[-1]
66
+ prop_name = path[-2]
67
+ section_path = path[:-2]
68
+ description = f"Item #{idx} in the '{prop_name}' array"
69
+ else:
70
+ prop_name = path[-1] if path else "value"
71
+ section_path = path[:-1]
72
+ description = f"'{prop_name}'"
73
+
74
+ suggestion = ""
75
+ if isinstance(error.instance, str) and all(isinstance(v, str) for v in valid_values):
76
+ match = _find_closest_match(error.instance, valid_values)
77
+ if match:
78
+ suggestion = f" Did you mean '{match}'?"
79
+
80
+ section = path_to_section_name(section_path)
81
+ valid_values_str = ", ".join(repr(v) for v in valid_values)
82
+ return (
83
+ f"Error in {section} section:\n Invalid value:\n\n"
84
+ f" - {description} → '{error.instance}' is not a valid value.{suggestion}\n\n"
85
+ f"Valid values are: {valid_values_str}."
86
+ )
87
+
88
+
89
+ def _format_type_error(error: ValidationError) -> str:
90
+ expected = error.validator_value
91
+ assert isinstance(expected, (str, list))
92
+ section = path_to_section_name(list(error.path)[:-1] if error.path else [])
93
+ assert error.path
94
+
95
+ type_phrases = {
96
+ "object": "an object",
97
+ "array": "an array",
98
+ "number": "a number",
99
+ "boolean": "a boolean",
100
+ "string": "a string",
101
+ "integer": "an integer",
102
+ "null": "null",
103
+ }
104
+ message = f"Error in {section} section:\n Type error:\n\n - '{error.path[-1]}' → Must be "
105
+
106
+ if isinstance(expected, list):
107
+ message += f"one of: {' or '.join(expected)}"
108
+ else:
109
+ message += type_phrases[expected]
110
+ actual = type(error.instance).__name__
111
+ message += f", but got {actual}: {error.instance}"
112
+ return message
113
+
114
+
115
+ def _format_additional_properties_error(error: ValidationError) -> str:
116
+ valid = list(error.schema.get("properties", {}))
117
+ unknown = sorted(set(error.instance) - set(valid))
118
+ valid_list = ", ".join(f"'{prop}'" for prop in valid)
119
+ section = path_to_section_name(list(error.path))
120
+
121
+ details = []
122
+ for prop in unknown:
123
+ match = _find_closest_match(prop, valid)
124
+ if match:
125
+ details.append(f"- '{prop}' → Did you mean '{match}'?")
126
+ else:
127
+ details.append(f"- '{prop}'")
128
+
129
+ return (
130
+ f"Error in {section} section:\n Unknown properties:\n\n"
131
+ + "\n".join(f" {detail}" for detail in details)
132
+ + f"\n\nValid properties for {section} are: {valid_list}."
133
+ )
134
+
135
+
136
+ def _format_anyof_error(error: ValidationError) -> str:
137
+ if list(error.schema_path) == ["properties", "operations", "items", "anyOf"]:
138
+ section = path_to_section_name(list(error.path))
139
+ return (
140
+ f"Error in {section} section:\n At least one filter is required when defining [[operations]].\n\n"
141
+ "Please specify at least one include or exclude filter property (e.g., include-path, exclude-tag, etc.)."
142
+ )
143
+ elif list(error.schema_path) == ["properties", "workers", "anyOf"]:
144
+ return (
145
+ f"Invalid value for 'workers': {error.instance!r}\n\n"
146
+ f"Expected either:\n"
147
+ f" - A positive integer (e.g., workers = 4)\n"
148
+ f' - The string "auto" for automatic detection (workers = "auto")'
149
+ )
150
+ return error.message
151
+
152
+
153
+ def path_to_section_name(path: list[int | str]) -> str:
154
+ """Convert a JSON path to a TOML-like section name."""
155
+ if not path:
156
+ return "root"
157
+
158
+ return f"[{'.'.join(str(p) for p in path)}]"
159
+
160
+
161
+ def _find_closest_match(value: str, variants: list[str]) -> str | None:
162
+ matches = difflib.get_close_matches(value, variants, n=1, cutoff=0.6)
163
+ return matches[0] if matches else None
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from schemathesis.config._diff_base import DiffBase
7
+ from schemathesis.generation.modes import GenerationMode
8
+
9
+ if TYPE_CHECKING:
10
+ from schemathesis.generation.metrics import MetricFunction
11
+
12
+
13
+ @dataclass(repr=False)
14
+ class GenerationConfig(DiffBase):
15
+ modes: list[GenerationMode]
16
+ max_examples: int | None
17
+ no_shrink: bool
18
+ deterministic: bool
19
+ # Allow generating `\x00` bytes in strings
20
+ allow_x00: bool
21
+ # Allow generating unexpected parameters in generated requests
22
+ allow_extra_parameters: bool
23
+ # Generate strings using the given codec
24
+ codec: str | None
25
+ maximize: list[MetricFunction]
26
+ # Whether to generate security parameters
27
+ with_security_parameters: bool
28
+ # Allowing using `null` for optional arguments in GraphQL queries
29
+ graphql_allow_null: bool
30
+ database: str | None
31
+ unique_inputs: bool
32
+ exclude_header_characters: str | None
33
+
34
+ __slots__ = (
35
+ "modes",
36
+ "max_examples",
37
+ "no_shrink",
38
+ "deterministic",
39
+ "allow_x00",
40
+ "allow_extra_parameters",
41
+ "codec",
42
+ "maximize",
43
+ "with_security_parameters",
44
+ "graphql_allow_null",
45
+ "database",
46
+ "unique_inputs",
47
+ "exclude_header_characters",
48
+ )
49
+
50
+ def __init__(
51
+ self,
52
+ *,
53
+ modes: list[GenerationMode] | None = None,
54
+ max_examples: int | None = None,
55
+ no_shrink: bool = False,
56
+ deterministic: bool = False,
57
+ allow_x00: bool = True,
58
+ allow_extra_parameters: bool = True,
59
+ codec: str | None = "utf-8",
60
+ maximize: list[MetricFunction] | None = None,
61
+ with_security_parameters: bool = True,
62
+ graphql_allow_null: bool = True,
63
+ database: str | None = None,
64
+ unique_inputs: bool = False,
65
+ exclude_header_characters: str | None = None,
66
+ ) -> None:
67
+ from schemathesis.generation import GenerationMode
68
+
69
+ self.modes = modes or list(GenerationMode)
70
+ self.max_examples = max_examples
71
+ self.no_shrink = no_shrink
72
+ self.deterministic = deterministic
73
+ self.allow_x00 = allow_x00
74
+ self.allow_extra_parameters = allow_extra_parameters
75
+ self.codec = codec
76
+ self.maximize = maximize or []
77
+ self.with_security_parameters = with_security_parameters
78
+ self.graphql_allow_null = graphql_allow_null
79
+ self.database = database
80
+ self.unique_inputs = unique_inputs
81
+ self.exclude_header_characters = exclude_header_characters
82
+
83
+ @classmethod
84
+ def from_dict(cls, data: dict[str, Any]) -> GenerationConfig:
85
+ mode_raw = data.get("mode")
86
+ if mode_raw == "all":
87
+ modes = list(GenerationMode)
88
+ elif mode_raw is not None:
89
+ modes = [GenerationMode(mode_raw)]
90
+ else:
91
+ modes = None
92
+ maximize = _get_maximize(data.get("maximize"))
93
+ return cls(
94
+ modes=modes,
95
+ max_examples=data.get("max-examples"),
96
+ no_shrink=data.get("no-shrink", False),
97
+ deterministic=data.get("deterministic", False),
98
+ allow_x00=data.get("allow-x00", True),
99
+ allow_extra_parameters=data.get("allow-extra-parameters", True),
100
+ codec=data.get("codec", "utf-8"),
101
+ maximize=maximize,
102
+ with_security_parameters=data.get("with-security-parameters", True),
103
+ graphql_allow_null=data.get("graphql-allow-null", True),
104
+ database=data.get("database"),
105
+ unique_inputs=data.get("unique-inputs", False),
106
+ exclude_header_characters=data.get("exclude-header-characters"),
107
+ )
108
+
109
+ def update(
110
+ self,
111
+ *,
112
+ modes: list[GenerationMode] | None = None,
113
+ max_examples: int | None = None,
114
+ no_shrink: bool | None = None,
115
+ deterministic: bool | None = None,
116
+ allow_x00: bool | None = None,
117
+ allow_extra_parameters: bool | None = None,
118
+ codec: str | None = None,
119
+ maximize: list[MetricFunction] | None = None,
120
+ with_security_parameters: bool | None = None,
121
+ graphql_allow_null: bool | None = None,
122
+ database: str | None = None,
123
+ unique_inputs: bool | None = None,
124
+ exclude_header_characters: str | None = None,
125
+ ) -> None:
126
+ if modes is not None:
127
+ self.modes = modes
128
+ if max_examples is not None:
129
+ self.max_examples = max_examples
130
+ self.no_shrink = no_shrink or False
131
+ self.deterministic = deterministic or False
132
+ self.allow_x00 = allow_x00 if allow_x00 is not None else True
133
+ self.allow_extra_parameters = allow_extra_parameters if allow_extra_parameters is not None else True
134
+ if codec is not None:
135
+ self.codec = codec
136
+ if maximize is not None:
137
+ self.maximize = maximize
138
+ if with_security_parameters is not None:
139
+ self.with_security_parameters = with_security_parameters
140
+ self.graphql_allow_null = graphql_allow_null if graphql_allow_null is not None else True
141
+ if database is not None:
142
+ self.database = database
143
+ self.unique_inputs = unique_inputs or False
144
+ if exclude_header_characters is not None:
145
+ self.exclude_header_characters = exclude_header_characters
146
+
147
+
148
+ def _get_maximize(value: Any) -> list[MetricFunction]:
149
+ from schemathesis.generation.metrics import METRICS
150
+
151
+ if isinstance(value, list):
152
+ metrics = value
153
+ elif isinstance(value, str):
154
+ metrics = [value]
155
+ else:
156
+ metrics = []
157
+ return METRICS.get_by_names(metrics)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, unique
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ import hypothesis
8
+
9
+
10
+ @unique
11
+ class HealthCheck(str, Enum):
12
+ data_too_large = "data_too_large"
13
+ filter_too_much = "filter_too_much"
14
+ too_slow = "too_slow"
15
+ large_base_example = "large_base_example"
16
+ all = "all"
17
+
18
+ def as_hypothesis(self) -> list[hypothesis.HealthCheck]:
19
+ from hypothesis import HealthCheck
20
+
21
+ if self.name == "all":
22
+ return list(HealthCheck)
23
+
24
+ return [HealthCheck[self.name]]