schemathesis 3.13.0__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 (245) 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 -1016
  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 +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  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 +753 -74
  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 +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  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.13.0.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.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ import http.client
4
+ import textwrap
5
+ import traceback
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+ from enum import Enum, auto
9
+ from json import JSONDecodeError
10
+ from typing import Any, Callable
11
+
12
+ from schemathesis.config import OutputConfig
13
+ from schemathesis.core.compat import BaseExceptionGroup
14
+ from schemathesis.core.output import prepare_response_payload
15
+ from schemathesis.core.transport import Response
16
+
17
+
18
+ class Severity(Enum):
19
+ # For server errors, security issues like ignored auth
20
+ CRITICAL = auto()
21
+ # For schema violations
22
+ HIGH = auto()
23
+ # For content type issues, header problems
24
+ MEDIUM = auto()
25
+ # For performance issues, minor inconsistencies
26
+ LOW = auto()
27
+
28
+ def __lt__(self, other: Severity) -> bool:
29
+ # Lower values are more severe
30
+ return self.value < other.value
31
+
32
+
33
+ @dataclass
34
+ class Failure(AssertionError):
35
+ """API check failure."""
36
+
37
+ __slots__ = ("operation", "title", "message", "case_id", "severity")
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ operation: str,
43
+ title: str,
44
+ message: str,
45
+ case_id: str | None = None,
46
+ severity: Severity = Severity.MEDIUM,
47
+ ) -> None:
48
+ self.operation = operation
49
+ self.title = title
50
+ self.message = message
51
+ self.case_id = case_id
52
+ self.severity = severity
53
+
54
+ def __str__(self) -> str:
55
+ if not self.message:
56
+ return self.title
57
+ return f"{self.title}\n\n{self.message}"
58
+
59
+ def __lt__(self, other: Failure) -> bool:
60
+ return (
61
+ self.severity,
62
+ self.__class__.__name__,
63
+ self.message,
64
+ ) < (other.severity, other.__class__.__name__, other.message)
65
+
66
+ # Comparison & hashing is done purely on classes to simplify keeping the minimized failure during shrinking
67
+ def __hash__(self) -> int:
68
+ return hash(self.__class__)
69
+
70
+ def __eq__(self, other: object, /) -> bool:
71
+ if not isinstance(other, Failure):
72
+ return NotImplemented
73
+ return type(self) is type(other) and self.operation == other.operation and self._unique_key == other._unique_key
74
+
75
+ @property
76
+ def _unique_key(self) -> Any:
77
+ return self.message
78
+
79
+
80
+ def get_origin(exception: BaseException, seen: tuple[BaseException, ...] = ()) -> tuple:
81
+ filename, lineno = None, None
82
+ if tb := exception.__traceback__:
83
+ filename, lineno, *_ = traceback.extract_tb(tb)[-1]
84
+ seen = (*seen, exception)
85
+ context = ()
86
+ if exception.__context__ is not None and exception.__context__ not in seen:
87
+ context = get_origin(exception.__context__, seen=seen)
88
+ return (
89
+ type(exception),
90
+ filename,
91
+ lineno,
92
+ context,
93
+ (
94
+ tuple(get_origin(exc, seen=seen) for exc in exception.exceptions if exc not in seen)
95
+ if isinstance(exception, BaseExceptionGroup)
96
+ else ()
97
+ ),
98
+ )
99
+
100
+
101
+ class CustomFailure(Failure):
102
+ __slots__ = ("operation", "title", "message", "exception", "case_id", "severity", "origin")
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ operation: str,
108
+ title: str,
109
+ message: str,
110
+ exception: AssertionError,
111
+ case_id: str | None = None,
112
+ severity: Severity = Severity.MEDIUM,
113
+ ) -> None:
114
+ self.operation = operation
115
+ self.title = title
116
+ self.message = message
117
+ self.exception = exception
118
+ self.case_id = case_id
119
+ self.severity = severity
120
+ self.origin = get_origin(exception)
121
+
122
+ @property
123
+ def _unique_key(self) -> Any:
124
+ return self.origin
125
+
126
+
127
+ class ResponseTimeExceeded(Failure):
128
+ """Response took longer than expected."""
129
+
130
+ __slots__ = ("operation", "elapsed", "deadline", "title", "message", "case_id", "severity")
131
+
132
+ def __init__(
133
+ self,
134
+ *,
135
+ operation: str,
136
+ elapsed: float,
137
+ deadline: float,
138
+ message: str,
139
+ title: str = "Response time limit exceeded",
140
+ case_id: str | None = None,
141
+ ) -> None:
142
+ self.operation = operation
143
+ self.elapsed = elapsed
144
+ self.deadline = deadline
145
+ self.title = title
146
+ self.message = message
147
+ self.case_id = case_id
148
+ self.severity = Severity.LOW
149
+
150
+ @property
151
+ def _unique_key(self) -> str:
152
+ return self.title
153
+
154
+
155
+ class ServerError(Failure):
156
+ """Server responded with an error."""
157
+
158
+ __slots__ = ("operation", "status_code", "title", "message", "case_id", "severity")
159
+
160
+ def __init__(
161
+ self,
162
+ *,
163
+ operation: str,
164
+ status_code: int,
165
+ title: str = "Server error",
166
+ message: str = "",
167
+ case_id: str | None = None,
168
+ ) -> None:
169
+ self.operation = operation
170
+ self.status_code = status_code
171
+ self.title = title
172
+ self.message = message
173
+ self.case_id = case_id
174
+ self.severity = Severity.CRITICAL
175
+
176
+ @property
177
+ def _unique_key(self) -> str:
178
+ return str(self.status_code)
179
+
180
+
181
+ class MalformedJson(Failure):
182
+ """Failed to deserialize JSON."""
183
+
184
+ __slots__ = (
185
+ "operation",
186
+ "validation_message",
187
+ "document",
188
+ "position",
189
+ "lineno",
190
+ "colno",
191
+ "message",
192
+ "title",
193
+ "case_id",
194
+ "severity",
195
+ )
196
+
197
+ def __init__(
198
+ self,
199
+ *,
200
+ operation: str,
201
+ validation_message: str,
202
+ document: str,
203
+ position: int,
204
+ lineno: int,
205
+ colno: int,
206
+ message: str,
207
+ title: str = "JSON deserialization error",
208
+ case_id: str | None = None,
209
+ ) -> None:
210
+ self.operation = operation
211
+ self.validation_message = validation_message
212
+ self.document = document
213
+ self.position = position
214
+ self.lineno = lineno
215
+ self.colno = colno
216
+ self.message = message
217
+ self.title = title
218
+ self.case_id = case_id
219
+ self.severity = Severity.MEDIUM
220
+
221
+ @property
222
+ def _unique_key(self) -> Any:
223
+ return self.title
224
+
225
+ @classmethod
226
+ def from_exception(cls, *, operation: str, exc: JSONDecodeError) -> MalformedJson:
227
+ message = f"Response must be valid JSON with 'Content-Type: application/json' header:\n\n {exc}"
228
+ return cls(
229
+ operation=operation,
230
+ message=message,
231
+ validation_message=exc.msg,
232
+ document=exc.doc,
233
+ position=exc.pos,
234
+ lineno=exc.lineno,
235
+ colno=exc.colno,
236
+ )
237
+
238
+
239
+ class FailureGroup(BaseExceptionGroup):
240
+ """Multiple distinct check failures."""
241
+
242
+ exceptions: Sequence[Failure]
243
+
244
+ def __init__(self, exceptions: Sequence[Failure], message: str = "", /) -> None:
245
+ super().__init__(message, exceptions)
246
+
247
+ def __new__(cls, failures: Sequence[Failure], message: str | None = None) -> FailureGroup:
248
+ if message is None:
249
+ message = failure_report_title(failures)
250
+ return super().__new__(cls, message, list(failures))
251
+
252
+
253
+ class MessageBlock(str, Enum):
254
+ CASE_ID = "case_id"
255
+ FAILURE = "failure"
256
+ STATUS = "status"
257
+ CURL = "curl"
258
+
259
+
260
+ BlockFormatter = Callable[[MessageBlock, str], str]
261
+
262
+
263
+ def failure_report_title(failures: Sequence[Failure]) -> str:
264
+ message = f"Schemathesis found {len(failures)} distinct failure"
265
+ if len(failures) > 1:
266
+ message += "s"
267
+ return message
268
+
269
+
270
+ def format_failures(
271
+ *,
272
+ case_id: str | None,
273
+ response: Response | None,
274
+ failures: Sequence[Failure],
275
+ curl: str,
276
+ formatter: BlockFormatter | None = None,
277
+ config: OutputConfig,
278
+ ) -> str:
279
+ """Format failure information with custom styling."""
280
+ formatter = formatter or (lambda _, x: x)
281
+
282
+ if case_id is not None:
283
+ output = formatter(MessageBlock.CASE_ID, f"{case_id}\n")
284
+ else:
285
+ output = ""
286
+
287
+ # Failures
288
+ for idx, failure in enumerate(failures):
289
+ output += formatter(MessageBlock.FAILURE, f"\n- {failure.title}")
290
+ if failure.message:
291
+ output += "\n\n"
292
+ output += textwrap.indent(failure.message, " ")
293
+ if idx != len(failures):
294
+ output += "\n"
295
+
296
+ # Response status
297
+ if isinstance(response, Response):
298
+ reason = http.client.responses.get(response.status_code, "Unknown")
299
+ output += formatter(MessageBlock.STATUS, f"\n[{response.status_code}] {reason}:\n")
300
+ # Response payload
301
+ if response.content is None or not response.content:
302
+ output += "\n <EMPTY>"
303
+ else:
304
+ try:
305
+ payload = prepare_response_payload(response.text, config=config)
306
+ output += textwrap.indent(f"\n`{payload}`", prefix=" ")
307
+ except UnicodeDecodeError:
308
+ output += "\n <BINARY>"
309
+ else:
310
+ output += "\n <NO RESPONSE>"
311
+
312
+ # cURL
313
+ _curl = "\n".join(f" {line}" for line in curl.splitlines())
314
+ output += "\n" + formatter(MessageBlock.CURL, f"\nReproduce with: \n\n{_curl}")
315
+
316
+ return output
@@ -0,0 +1,19 @@
1
+ import os
2
+ import pathlib
3
+
4
+
5
+ def ensure_parent(path: os.PathLike, fail_silently: bool = True) -> None:
6
+ # Try to create the parent dir
7
+ try:
8
+ pathlib.Path(path).parent.mkdir(mode=0o755, parents=True, exist_ok=True)
9
+ except OSError:
10
+ if not fail_silently:
11
+ raise
12
+
13
+
14
+ def file_exists(path: str) -> bool:
15
+ try:
16
+ return pathlib.Path(path).is_file()
17
+ except OSError:
18
+ # For example, path could be too long
19
+ return False
@@ -0,0 +1,20 @@
1
+ import os
2
+ import sys
3
+
4
+ from schemathesis.core.errors import HookError
5
+
6
+ HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
7
+
8
+
9
+ def load_from_env() -> None:
10
+ hooks = os.getenv(HOOKS_MODULE_ENV_VAR)
11
+ if hooks:
12
+ load_from_path(hooks)
13
+
14
+
15
+ def load_from_path(module_path: str) -> None:
16
+ try:
17
+ sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
18
+ __import__(module_path)
19
+ except Exception as exc:
20
+ raise HookError(module_path) from exc
@@ -0,0 +1,13 @@
1
+ from .bundler import BUNDLE_STORAGE_KEY, REFERENCE_TO_BUNDLE_PREFIX, BundleError, Bundler, bundle
2
+ from .keywords import ALL_KEYWORDS
3
+ from .types import get_type
4
+
5
+ __all__ = [
6
+ "ALL_KEYWORDS",
7
+ "bundle",
8
+ "Bundler",
9
+ "BundleError",
10
+ "REFERENCE_TO_BUNDLE_PREFIX",
11
+ "BUNDLE_STORAGE_KEY",
12
+ "get_type",
13
+ ]
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from schemathesis.core.errors import InfiniteRecursiveReference
7
+ from schemathesis.core.jsonschema.references import sanitize
8
+ from schemathesis.core.jsonschema.types import JsonSchema, to_json_type_name
9
+ from schemathesis.core.transforms import deepclone
10
+
11
+ if TYPE_CHECKING:
12
+ from schemathesis.core.compat import RefResolver
13
+
14
+
15
+ BUNDLE_STORAGE_KEY = "x-bundled"
16
+ REFERENCE_TO_BUNDLE_PREFIX = f"#/{BUNDLE_STORAGE_KEY}"
17
+
18
+
19
+ class BundleError(Exception):
20
+ def __init__(self, reference: str, value: Any) -> None:
21
+ self.reference = reference
22
+ self.value = value
23
+
24
+ def __str__(self) -> str:
25
+ return f"Cannot bundle `{self.reference}`: expected JSON Schema (object or boolean), got {to_json_type_name(self.value)}"
26
+
27
+
28
+ @dataclass
29
+ class Bundle:
30
+ schema: JsonSchema
31
+ name_to_uri: dict[str, str]
32
+
33
+ __slots__ = ("schema", "name_to_uri")
34
+
35
+
36
+ class Bundler:
37
+ """Bundler tracks schema ids stored in a bundle."""
38
+
39
+ counter: int
40
+
41
+ __slots__ = ("counter",)
42
+
43
+ def __init__(self) -> None:
44
+ self.counter = 0
45
+
46
+ def bundle(self, schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> Bundle:
47
+ """Bundle a JSON Schema by embedding all references."""
48
+ # Inlining recursive reference is required (for now) for data generation, but is unsound for data validation
49
+ if not isinstance(schema, dict):
50
+ return Bundle(schema=schema, name_to_uri={})
51
+
52
+ # Track visited URIs and their local definition names
53
+ inlining_for_recursion: set[str] = set()
54
+ visited: set[str] = set()
55
+ uri_to_name: dict[str, str] = {}
56
+ defs = {}
57
+
58
+ has_recursive_references = False
59
+ resolve = resolver.resolve
60
+ visit = visited.add
61
+
62
+ def get_def_name(uri: str) -> str:
63
+ """Generate or retrieve the local definition name for a URI."""
64
+ name = uri_to_name.get(uri)
65
+ if name is None:
66
+ self.counter += 1
67
+ name = f"schema{self.counter}"
68
+ uri_to_name[uri] = name
69
+ return name
70
+
71
+ def bundle_recursive(current: JsonSchema | list[JsonSchema]) -> JsonSchema | list[JsonSchema]:
72
+ """Recursively process and bundle references in the current schema."""
73
+ # Local lookup is cheaper and it matters for large schemas.
74
+ # It works because this recursive call goes to every nested value
75
+ nonlocal has_recursive_references
76
+ _bundle_recursive = bundle_recursive
77
+ if isinstance(current, dict):
78
+ reference = current.get("$ref")
79
+ if isinstance(reference, str) and not reference.startswith(REFERENCE_TO_BUNDLE_PREFIX):
80
+ resolved_uri, resolved_schema = resolve(reference)
81
+
82
+ if not isinstance(resolved_schema, (dict, bool)):
83
+ raise BundleError(reference, resolved_schema)
84
+ def_name = get_def_name(resolved_uri)
85
+
86
+ scopes = resolver._scopes_stack
87
+
88
+ is_recursive_reference = resolved_uri in scopes
89
+ has_recursive_references |= is_recursive_reference
90
+ if inline_recursive and is_recursive_reference:
91
+ # This is a recursive reference! As of Sep 2025, `hypothesis-jsonschema` does not support
92
+ # recursive references and Schemathesis has to remove them if possible.
93
+ #
94
+ # Cutting them of immediately would limit the quality of generated data, since it would have
95
+ # just a single level of recursion. Currently, the only way to generate recursive data is to
96
+ # inline definitions directly, which can lead to schema size explosion.
97
+ #
98
+ # To balance it, Schemathesis inlines one level, that avoids exponential blowup of O(B ^ L)
99
+ # in worst case, where B is branching factor (number of recursive references per schema), and
100
+ # L is the number of levels. Even quadratic growth can be unacceptable for large schemas.
101
+ #
102
+ # In the future, it **should** be handled by `hypothesis-jsonschema` instead.
103
+ if resolved_uri in inlining_for_recursion:
104
+ # Check if we're already trying to inline this schema
105
+ # If yes, it means we have an unbreakable cycle
106
+ cycle = scopes[scopes.index(resolved_uri) :]
107
+ raise InfiniteRecursiveReference(reference, cycle)
108
+
109
+ # Track that we're inlining this schema
110
+ inlining_for_recursion.add(resolved_uri)
111
+ try:
112
+ cloned = deepclone(resolved_schema)
113
+ # Sanitize to remove optional recursive references
114
+ sanitize(cloned)
115
+
116
+ result = {key: _bundle_recursive(value) for key, value in current.items() if key != "$ref"}
117
+ bundled_clone = _bundle_recursive(cloned)
118
+ assert isinstance(bundled_clone, dict)
119
+ result.update(bundled_clone)
120
+ return result
121
+ finally:
122
+ inlining_for_recursion.discard(resolved_uri)
123
+ elif resolved_uri not in visited:
124
+ # Bundle only new schemas
125
+ visit(resolved_uri)
126
+
127
+ # Recursively bundle the embedded schema too!
128
+ resolver.push_scope(resolved_uri)
129
+ try:
130
+ bundled_resolved = _bundle_recursive(resolved_schema)
131
+ finally:
132
+ resolver.pop_scope()
133
+
134
+ defs[def_name] = bundled_resolved
135
+
136
+ return {
137
+ key: f"{REFERENCE_TO_BUNDLE_PREFIX}/{def_name}"
138
+ if key == "$ref"
139
+ else _bundle_recursive(value)
140
+ if isinstance(value, (dict, list))
141
+ else value
142
+ for key, value in current.items()
143
+ }
144
+ else:
145
+ # Already visited - just update $ref
146
+ return {
147
+ key: f"{REFERENCE_TO_BUNDLE_PREFIX}/{def_name}"
148
+ if key == "$ref"
149
+ else _bundle_recursive(value)
150
+ if isinstance(value, (dict, list))
151
+ else value
152
+ for key, value in current.items()
153
+ }
154
+ return {
155
+ key: _bundle_recursive(value) if isinstance(value, (dict, list)) else value
156
+ for key, value in current.items()
157
+ }
158
+ elif isinstance(current, list):
159
+ return [_bundle_recursive(item) if isinstance(item, (dict, list)) else item for item in current] # type: ignore[misc]
160
+ # `isinstance` guards won't let it happen
161
+ # Otherwise is present to make type checker happy
162
+ return current # pragma: no cover
163
+
164
+ bundled = bundle_recursive(schema)
165
+
166
+ assert isinstance(bundled, dict)
167
+
168
+ # Inlining such a schema is only possible if recursive references were inlined
169
+ if (inline_recursive or not has_recursive_references) and "$ref" in bundled and len(defs) == 1:
170
+ result = {key: value for key, value in bundled.items() if key != "$ref"}
171
+ for value in defs.values():
172
+ if isinstance(value, dict):
173
+ result.update(value)
174
+ return Bundle(schema=result, name_to_uri={})
175
+
176
+ if defs:
177
+ bundled[BUNDLE_STORAGE_KEY] = defs
178
+ return Bundle(schema=bundled, name_to_uri={v: k for k, v in uri_to_name.items()})
179
+
180
+
181
+ def bundle(schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> Bundle:
182
+ """Bundle a JSON Schema by embedding all references."""
183
+ return Bundler().bundle(schema, resolver, inline_recursive=inline_recursive)
@@ -0,0 +1,40 @@
1
+ ALL_KEYWORDS = frozenset(
2
+ {
3
+ "additionalItems",
4
+ "additionalProperties",
5
+ "allOf",
6
+ "anyOf",
7
+ "const",
8
+ "contains",
9
+ "contentEncoding",
10
+ "contentMediaType",
11
+ "dependencies",
12
+ "enum",
13
+ "else",
14
+ "exclusiveMaximum",
15
+ "exclusiveMinimum",
16
+ "format",
17
+ "if",
18
+ "items",
19
+ "maxItems",
20
+ "maxLength",
21
+ "maxProperties",
22
+ "maximum",
23
+ "minItems",
24
+ "minLength",
25
+ "minProperties",
26
+ "minimum",
27
+ "multipleOf",
28
+ "not",
29
+ "oneOf",
30
+ "pattern",
31
+ "patternProperties",
32
+ "properties",
33
+ "propertyNames",
34
+ "$ref",
35
+ "required",
36
+ "then",
37
+ "type",
38
+ "uniqueItems",
39
+ }
40
+ )