schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,370 @@
1
+ """Base error handling that is not tied to any specific API specification or execution context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ import re
7
+ import traceback
8
+ from types import TracebackType
9
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn
10
+
11
+ from schemathesis.core.output import truncate_json
12
+
13
+ if TYPE_CHECKING:
14
+ from jsonschema import SchemaError as JsonSchemaError
15
+ from jsonschema import ValidationError
16
+ from requests import RequestException
17
+
18
+ from schemathesis.core.compat import RefResolutionError
19
+
20
+
21
+ SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
22
+ SERIALIZERS_SUGGESTION_MESSAGE = (
23
+ "You can register your own serializer with `schemathesis.serializer` "
24
+ "and Schemathesis will be able to make API calls with this media type. \n"
25
+ "See https://schemathesis.readthedocs.io/en/stable/how.html#payload-serialization for more information."
26
+ )
27
+
28
+
29
+ class SchemathesisError(Exception):
30
+ """Base exception class for all Schemathesis errors."""
31
+
32
+
33
+ class InvalidSchema(SchemathesisError):
34
+ """Indicates errors in API schema validation or processing."""
35
+
36
+ def __init__(
37
+ self,
38
+ message: str | None = None,
39
+ path: str | None = None,
40
+ method: str | None = None,
41
+ full_path: str | None = None,
42
+ ) -> None:
43
+ self.message = message
44
+ self.path = path
45
+ self.method = method
46
+ self.full_path = full_path
47
+
48
+ @classmethod
49
+ def from_jsonschema_error(
50
+ cls, error: ValidationError, path: str | None, method: str | None, full_path: str | None
51
+ ) -> InvalidSchema:
52
+ if error.absolute_path:
53
+ part = error.absolute_path[-1]
54
+ if isinstance(part, int) and len(error.absolute_path) > 1:
55
+ parent = error.absolute_path[-2]
56
+ message = f"Invalid definition for element at index {part} in `{parent}`"
57
+ else:
58
+ message = f"Invalid `{part}` definition"
59
+ else:
60
+ message = "Invalid schema definition"
61
+ error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
62
+ message += f"\n\nLocation:\n {error_path}"
63
+ instance = truncate_json(error.instance)
64
+ message += f"\n\nProblematic definition:\n{instance}"
65
+ message += "\n\nError details:\n "
66
+ # This default message contains the instance which we already printed
67
+ if "is not valid under any of the given schemas" in error.message:
68
+ message += "The provided definition doesn't match any of the expected formats or types."
69
+ else:
70
+ message += error.message
71
+ message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
72
+ return cls(message, path=path, method=method, full_path=full_path)
73
+
74
+ @classmethod
75
+ def from_reference_resolution_error(
76
+ cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
77
+ ) -> InvalidSchema:
78
+ notes = getattr(error, "__notes__", [])
79
+ # Some exceptions don't have the actual reference in them, hence we add it manually via notes
80
+ pointer = f"'{notes[0]}'"
81
+ message = "Unresolvable JSON pointer in the schema"
82
+ # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
83
+ message += f"\n\nError details:\n JSON pointer: {pointer}"
84
+ message += "\n This typically means that the schema is referencing a component that doesn't exist."
85
+ message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
86
+ return cls(message, path=path, method=method, full_path=full_path)
87
+
88
+ def as_failing_test_function(self) -> Callable:
89
+ """Create a test function that will fail.
90
+
91
+ This approach allows us to use default pytest reporting style for operation-level schema errors.
92
+ """
93
+
94
+ def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
95
+ __tracebackhide__ = True
96
+ raise self
97
+
98
+ return actual_test
99
+
100
+
101
+ class InvalidRegexType(InvalidSchema):
102
+ """Raised when an invalid type is used where a regex pattern is expected."""
103
+
104
+
105
+ class MalformedMediaType(ValueError):
106
+ """Raised on parsing of incorrect media type."""
107
+
108
+
109
+ class InvalidRegexPattern(InvalidSchema):
110
+ """Raised when a string pattern is not a valid regular expression."""
111
+
112
+ @classmethod
113
+ def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegexPattern:
114
+ match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
115
+ if match:
116
+ message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
117
+ return cls(message)
118
+
119
+ @classmethod
120
+ def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegexPattern:
121
+ if from_examples:
122
+ message = (
123
+ "Failed to generate test cases from examples for this API operation because of "
124
+ f"unsupported regular expression `{error.instance}`"
125
+ )
126
+ else:
127
+ message = (
128
+ "Failed to generate test cases for this API operation because of "
129
+ f"unsupported regular expression `{error.instance}`"
130
+ )
131
+ return cls(message)
132
+
133
+
134
+ class InvalidHeadersExample(InvalidSchema):
135
+ @classmethod
136
+ def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
137
+ message = (
138
+ "Failed to generate test cases from examples for this API operation because of "
139
+ "some header examples are invalid:\n"
140
+ )
141
+ for key, value in headers.items():
142
+ message += f"\n - {key!r}={value!r}"
143
+ message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
144
+ return cls(message)
145
+
146
+
147
+ class IncorrectUsage(SchemathesisError):
148
+ """Indicates incorrect usage of Schemathesis' public API."""
149
+
150
+
151
+ class InvalidRateLimit(IncorrectUsage):
152
+ """Incorrect input for rate limiting."""
153
+
154
+ def __init__(self, value: str) -> None:
155
+ super().__init__(
156
+ f"Invalid rate limit value: `{value}`. Should be in form `limit/interval`. "
157
+ "Example: `10/m` for 10 requests per minute."
158
+ )
159
+
160
+
161
+ class InternalError(SchemathesisError):
162
+ """Internal error in Schemathesis."""
163
+
164
+
165
+ class SerializationError(SchemathesisError):
166
+ """Can't serialize request payload."""
167
+
168
+
169
+ NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
170
+ UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
171
+ "Unbound prefix: `{prefix}`. "
172
+ "You need to define this namespace in your API schema via the `xml.namespace` keyword. "
173
+ f"See more at {NAMESPACE_DEFINITION_URL}"
174
+ )
175
+
176
+
177
+ class UnboundPrefix(SerializationError):
178
+ """XML serialization error.
179
+
180
+ It happens when the schema does not define a namespace that is used by some of its parts.
181
+ """
182
+
183
+ def __init__(self, prefix: str):
184
+ super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
185
+
186
+
187
+ SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
188
+ f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
189
+ )
190
+ SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
191
+ f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
192
+ )
193
+ RECURSIVE_REFERENCE_ERROR_MESSAGE = (
194
+ "Currently, Schemathesis can't generate data for this operation due to "
195
+ "recursive references in the operation definition. See more information in "
196
+ "this issue - https://github.com/schemathesis/schemathesis/issues/947"
197
+ )
198
+
199
+
200
+ class SerializationNotPossible(SerializationError):
201
+ """Not possible to serialize data to specified media type(s).
202
+
203
+ This error occurs in two scenarios:
204
+ 1. When attempting to serialize to a specific media type that isn't supported
205
+ 2. When none of the available media types can be used for serialization
206
+ """
207
+
208
+ def __init__(self, message: str, media_types: list[str]) -> None:
209
+ self.message = message
210
+ self.media_types = media_types
211
+
212
+ def __str__(self) -> str:
213
+ return self.message
214
+
215
+ @classmethod
216
+ def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
217
+ """Create error when no available media type can be used."""
218
+ return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
219
+
220
+ @classmethod
221
+ def for_media_type(cls, media_type: str) -> SerializationNotPossible:
222
+ """Create error when a specific required media type isn't supported."""
223
+ return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
224
+
225
+
226
+ class OperationNotFound(LookupError, SchemathesisError):
227
+ """Raised when an API operation cannot be found in the schema.
228
+
229
+ This error typically occurs during schema access in user code when trying to
230
+ reference a non-existent operation.
231
+ """
232
+
233
+ def __init__(self, message: str, item: str) -> None:
234
+ self.message = message
235
+ self.item = item
236
+
237
+ def __str__(self) -> str:
238
+ return self.message
239
+
240
+
241
+ @enum.unique
242
+ class LoaderErrorKind(str, enum.Enum):
243
+ # Connection related issues
244
+ CONNECTION_SSL = "connection_ssl"
245
+ CONNECTION_OTHER = "connection_other"
246
+ NETWORK_OTHER = "network_other"
247
+
248
+ # HTTP error codes
249
+ HTTP_SERVER_ERROR = "http_server_error"
250
+ HTTP_CLIENT_ERROR = "http_client_error"
251
+ HTTP_NOT_FOUND = "http_not_found"
252
+ HTTP_FORBIDDEN = "http_forbidden"
253
+
254
+ # Content decoding issues
255
+ SYNTAX_ERROR = "syntax_error"
256
+ UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
257
+ YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
258
+ YAML_NON_STRING_KEYS = "yaml_non_string_keys"
259
+
260
+ # Open API validation
261
+ OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
262
+ OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
263
+ OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
264
+
265
+ # GraphQL validation
266
+ GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
267
+
268
+ # Unclassified
269
+ UNCLASSIFIED = "unclassified"
270
+
271
+
272
+ class LoaderError(SchemathesisError):
273
+ """Failed to load an API schema."""
274
+
275
+ def __init__(
276
+ self,
277
+ kind: LoaderErrorKind,
278
+ message: str,
279
+ url: str | None = None,
280
+ extras: list[str] | None = None,
281
+ ) -> None:
282
+ self.kind = kind
283
+ self.message = message
284
+ self.url = url
285
+ self.extras = extras or []
286
+
287
+ def __str__(self) -> str:
288
+ return self.message
289
+
290
+
291
+ def get_request_error_extras(exc: RequestException) -> list[str]:
292
+ """Extract additional context from a request exception."""
293
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
294
+ from urllib3.exceptions import MaxRetryError
295
+
296
+ if isinstance(exc, SSLError):
297
+ reason = str(exc.args[0].reason)
298
+ return [_remove_ssl_line_number(reason).strip()]
299
+ if isinstance(exc, ConnectionError):
300
+ inner = exc.args[0]
301
+ if isinstance(inner, MaxRetryError) and inner.reason is not None:
302
+ arg = inner.reason.args[0]
303
+ if isinstance(arg, str):
304
+ if ":" not in arg:
305
+ reason = arg
306
+ else:
307
+ _, reason = arg.split(":", maxsplit=1)
308
+ else:
309
+ reason = f"Max retries exceeded with url: {inner.url}"
310
+ return [reason.strip()]
311
+ return [" ".join(map(_clean_inner_request_message, inner.args))]
312
+ if isinstance(exc, ChunkedEncodingError):
313
+ return [str(exc.args[0].args[1])]
314
+ return []
315
+
316
+
317
+ def _remove_ssl_line_number(text: str) -> str:
318
+ return re.sub(r"\(_ssl\.c:\d+\)", "", text)
319
+
320
+
321
+ def _clean_inner_request_message(message: object) -> str:
322
+ if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
323
+ return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
324
+ return str(message)
325
+
326
+
327
+ def get_request_error_message(exc: RequestException) -> str:
328
+ """Extract user-facing message from a request exception."""
329
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, ReadTimeout, SSLError
330
+
331
+ if isinstance(exc, ReadTimeout):
332
+ _, duration = exc.args[0].args[0][:-1].split("read timeout=")
333
+ return f"Read timed out after {duration} seconds"
334
+ if isinstance(exc, SSLError):
335
+ return "SSL verification problem"
336
+ if isinstance(exc, ConnectionError):
337
+ return "Connection failed"
338
+ if isinstance(exc, ChunkedEncodingError):
339
+ return "Connection broken. The server declared chunked encoding but sent an invalid chunk"
340
+ return str(exc)
341
+
342
+
343
+ def split_traceback(traceback: str) -> list[str]:
344
+ return [entry for entry in traceback.splitlines() if entry]
345
+
346
+
347
+ def format_exception(
348
+ error: Exception,
349
+ *,
350
+ with_traceback: bool = False,
351
+ skip_frames: int = 0,
352
+ ) -> str:
353
+ """Format exception with optional traceback."""
354
+ if not with_traceback:
355
+ lines = traceback.format_exception_only(type(error), error)
356
+ return "".join(lines).strip()
357
+
358
+ trace = error.__traceback__
359
+ if skip_frames > 0:
360
+ trace = extract_nth_traceback(trace, skip_frames)
361
+ lines = traceback.format_exception(type(error), error, trace)
362
+ return "".join(lines).strip()
363
+
364
+
365
+ def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
366
+ depth = 0
367
+ while depth < n and trace is not None:
368
+ trace = trace.tb_next
369
+ depth += 1
370
+ return trace
@@ -0,0 +1,285 @@
1
+ from __future__ import annotations
2
+
3
+ import http.client
4
+ import textwrap
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from enum import Enum, auto
8
+ from json import JSONDecodeError
9
+ from typing import Callable
10
+
11
+ from schemathesis.core.compat import BaseExceptionGroup
12
+ from schemathesis.core.output import OutputConfig, prepare_response_payload
13
+ from schemathesis.core.transport import Response
14
+
15
+
16
+ class Severity(Enum):
17
+ # For server errors, security issues like ignored auth
18
+ CRITICAL = auto()
19
+ # For schema violations
20
+ HIGH = auto()
21
+ # For content type issues, header problems
22
+ MEDIUM = auto()
23
+ # For performance issues, minor inconsistencies
24
+ LOW = auto()
25
+
26
+ def __lt__(self, other: Severity) -> bool:
27
+ # Lower values are more severe
28
+ return self.value < other.value
29
+
30
+
31
+ @dataclass
32
+ class Failure(AssertionError):
33
+ """API check failure."""
34
+
35
+ __slots__ = ("operation", "title", "message", "code", "case_id", "severity")
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ operation: str,
41
+ title: str,
42
+ message: str,
43
+ code: str,
44
+ case_id: str | None = None,
45
+ severity: Severity = Severity.MEDIUM,
46
+ ) -> None:
47
+ self.operation = operation
48
+ self.title = title
49
+ self.message = message
50
+ self.code = code
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
+ @classmethod
76
+ def from_assertion(cls, *, name: str, operation: str, exc: AssertionError) -> Failure:
77
+ return Failure(
78
+ operation=operation,
79
+ title=f"Custom check failed: `{name}`",
80
+ message=str(exc),
81
+ code="custom",
82
+ )
83
+
84
+ @property
85
+ def _unique_key(self) -> str:
86
+ return self.message
87
+
88
+
89
+ @dataclass
90
+ class MaxResponseTimeConfig:
91
+ limit: float = 10.0
92
+
93
+
94
+ class ResponseTimeExceeded(Failure):
95
+ """Response took longer than expected."""
96
+
97
+ __slots__ = ("operation", "elapsed", "deadline", "title", "message", "code", "case_id", "severity")
98
+
99
+ def __init__(
100
+ self,
101
+ *,
102
+ operation: str,
103
+ elapsed: float,
104
+ deadline: int,
105
+ message: str,
106
+ title: str = "Response time limit exceeded",
107
+ code: str = "response_time_exceeded",
108
+ case_id: str | None = None,
109
+ ) -> None:
110
+ self.operation = operation
111
+ self.elapsed = elapsed
112
+ self.deadline = deadline
113
+ self.title = title
114
+ self.message = message
115
+ self.code = code
116
+ self.case_id = case_id
117
+ self.severity = Severity.LOW
118
+
119
+ @property
120
+ def _unique_key(self) -> str:
121
+ return self.title
122
+
123
+
124
+ class ServerError(Failure):
125
+ """Server responded with an error."""
126
+
127
+ __slots__ = ("operation", "status_code", "title", "message", "code", "case_id", "severity")
128
+
129
+ def __init__(
130
+ self,
131
+ *,
132
+ operation: str,
133
+ status_code: int,
134
+ title: str = "Server error",
135
+ message: str = "",
136
+ code: str = "server_error",
137
+ case_id: str | None = None,
138
+ ) -> None:
139
+ self.operation = operation
140
+ self.status_code = status_code
141
+ self.title = title
142
+ self.message = message
143
+ self.code = code
144
+ self.case_id = case_id
145
+ self.severity = Severity.CRITICAL
146
+
147
+ @property
148
+ def _unique_key(self) -> str:
149
+ return str(self.status_code)
150
+
151
+
152
+ class MalformedJson(Failure):
153
+ """Failed to deserialize JSON."""
154
+
155
+ __slots__ = (
156
+ "operation",
157
+ "validation_message",
158
+ "document",
159
+ "position",
160
+ "lineno",
161
+ "colno",
162
+ "message",
163
+ "title",
164
+ "code",
165
+ "case_id",
166
+ "severity",
167
+ )
168
+
169
+ def __init__(
170
+ self,
171
+ *,
172
+ operation: str,
173
+ validation_message: str,
174
+ document: str,
175
+ position: int,
176
+ lineno: int,
177
+ colno: int,
178
+ message: str,
179
+ title: str = "JSON deserialization error",
180
+ code: str = "malformed_json",
181
+ case_id: str | None = None,
182
+ ) -> None:
183
+ self.operation = operation
184
+ self.validation_message = validation_message
185
+ self.document = document
186
+ self.position = position
187
+ self.lineno = lineno
188
+ self.colno = colno
189
+ self.message = message
190
+ self.title = title
191
+ self.code = code
192
+ self.case_id = case_id
193
+ self.severity = Severity.MEDIUM
194
+
195
+ @property
196
+ def _unique_key(self) -> str:
197
+ return self.title
198
+
199
+ @classmethod
200
+ def from_exception(cls, *, operation: str, exc: JSONDecodeError) -> MalformedJson:
201
+ return cls(
202
+ operation=operation,
203
+ message=str(exc),
204
+ validation_message=exc.msg,
205
+ document=exc.doc,
206
+ position=exc.pos,
207
+ lineno=exc.lineno,
208
+ colno=exc.colno,
209
+ )
210
+
211
+
212
+ class FailureGroup(BaseExceptionGroup):
213
+ """Multiple distinct check failures."""
214
+
215
+ exceptions: Sequence[Failure]
216
+
217
+ def __new__(cls, failures: Sequence[Failure], message: str | None = None) -> FailureGroup:
218
+ if message is None:
219
+ message = failure_report_title(failures)
220
+ return super().__new__(cls, message, list(failures))
221
+
222
+
223
+ class MessageBlock(Enum):
224
+ CASE_ID = "case_id"
225
+ FAILURE = "failure"
226
+ STATUS = "status"
227
+ CURL = "curl"
228
+
229
+
230
+ BlockFormatter = Callable[[MessageBlock, str], str]
231
+
232
+
233
+ def failure_report_title(failures: Sequence[Failure]) -> str:
234
+ message = f"Schemathesis found {len(failures)} distinct failure"
235
+ if len(failures) > 1:
236
+ message += "s"
237
+ return message
238
+
239
+
240
+ def format_failures(
241
+ *,
242
+ case_id: str | None,
243
+ response: Response | None,
244
+ failures: Sequence[Failure],
245
+ curl: str,
246
+ formatter: BlockFormatter | None = None,
247
+ config: OutputConfig,
248
+ ) -> str:
249
+ """Format failure information with custom styling."""
250
+ formatter = formatter or (lambda _, x: x)
251
+
252
+ if case_id is not None:
253
+ output = formatter(MessageBlock.CASE_ID, f"{case_id}\n")
254
+ else:
255
+ output = ""
256
+
257
+ # Failures
258
+ for idx, failure in enumerate(failures):
259
+ output += formatter(MessageBlock.FAILURE, f"\n- {failure.title}")
260
+ if failure.message:
261
+ output += "\n\n"
262
+ output += textwrap.indent(failure.message, " ")
263
+ if idx != len(failures):
264
+ output += "\n"
265
+
266
+ # Response status
267
+ if isinstance(response, Response):
268
+ reason = http.client.responses.get(response.status_code, "Unknown")
269
+ output += formatter(MessageBlock.STATUS, f"\n[{response.status_code}] {reason}:\n")
270
+ # Response payload
271
+ if response.content is None or not response.content:
272
+ output += "\n <EMPTY>"
273
+ else:
274
+ try:
275
+ payload = prepare_response_payload(response.text, config=config)
276
+ output += textwrap.indent(f"\n`{payload}`", prefix=" ")
277
+ except UnicodeDecodeError:
278
+ output += "\n <BINARY>"
279
+ else:
280
+ output += "\n <NO RESPONSE>"
281
+
282
+ # cURL
283
+ output += "\n" + formatter(MessageBlock.CURL, f"\nReproduce with: \n\n {curl}")
284
+
285
+ 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
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any, Callable
3
4
 
4
5