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,588 @@
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 dataclasses import dataclass
9
+ from textwrap import indent
10
+ from types import TracebackType
11
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn
12
+
13
+ from schemathesis.core.output import truncate_json
14
+
15
+ if TYPE_CHECKING:
16
+ from jsonschema import SchemaError as JsonSchemaError
17
+ from jsonschema import ValidationError
18
+ from requests import RequestException
19
+
20
+ from schemathesis.config import OutputConfig
21
+ from schemathesis.core.compat import RefResolutionError
22
+ from schemathesis.core.jsonschema import BundleError
23
+
24
+
25
+ SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
26
+ SERIALIZERS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/custom-serializers/"
27
+ STATEFUL_TESTING_GUIDE_URL = "https://schemathesis.readthedocs.io/en/stable/guides/stateful-testing/"
28
+ SERIALIZERS_SUGGESTION_MESSAGE = f"Check your schema or add custom serializers: {SERIALIZERS_DOCUMENTATION_URL}"
29
+ SERIALIZATION_NOT_POSSIBLE_MESSAGE = f"No supported serializers for media types: {{}}\n{SERIALIZERS_SUGGESTION_MESSAGE}"
30
+ SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
31
+ f"Cannot serialize to '{{}}' (unsupported media type)\n{SERIALIZERS_SUGGESTION_MESSAGE}"
32
+ )
33
+
34
+
35
+ class SchemathesisError(Exception):
36
+ """Base exception class for all Schemathesis errors."""
37
+
38
+
39
+ class DefinitionKind(str, enum.Enum):
40
+ SCHEMA = "Schema Object"
41
+ SECURITY_SCHEME = "Security Scheme Object"
42
+ RESPONSES = "Responses Object"
43
+ PARAMETER = "Parameter Object"
44
+
45
+
46
+ @dataclass
47
+ class SchemaLocation:
48
+ kind: DefinitionKind
49
+ # Hint about where the definition is located
50
+ hint: str | None
51
+ # Open API spec version
52
+ version: str
53
+
54
+ __slots__ = ("kind", "hint", "version")
55
+
56
+ @classmethod
57
+ def response_schema(cls, version: str) -> SchemaLocation:
58
+ return cls(kind=DefinitionKind.SCHEMA, hint="in response definition", version=version)
59
+
60
+ @classmethod
61
+ def maybe_from_error_path(cls, path: list[str | int], version: str) -> SchemaLocation | None:
62
+ if len(path) == 3 and path[:2] == ["components", "securitySchemes"]:
63
+ return cls(kind=DefinitionKind.SECURITY_SCHEME, hint=f"definition for `{path[2]}`", version=version)
64
+ if len(path) == 3 and path[:2] == ["components", "schemas"]:
65
+ return cls(kind=DefinitionKind.SCHEMA, hint=f"definition for `{path[2]}`", version=version)
66
+ if len(path) == 4 and path[0] == "paths" and path[-1] == "responses":
67
+ return cls(kind=DefinitionKind.RESPONSES, hint=None, version=version)
68
+ if len(path) == 5 and path[0] == "paths" and path[3] == "parameters":
69
+ return cls(kind=DefinitionKind.PARAMETER, hint=f"at index {path[4]}", version=version)
70
+
71
+ return None
72
+
73
+ @property
74
+ def message(self) -> str:
75
+ message = f"Invalid {self.kind.value}"
76
+ if self.hint is not None:
77
+ message += f" {self.hint}"
78
+ else:
79
+ message += " definition"
80
+ return message
81
+
82
+ @property
83
+ def specification_url(self) -> str:
84
+ anchor = {
85
+ DefinitionKind.SCHEMA: "schema-object",
86
+ DefinitionKind.SECURITY_SCHEME: "security-scheme-object",
87
+ DefinitionKind.RESPONSES: "responses-object",
88
+ DefinitionKind.PARAMETER: "parameter-object",
89
+ }[self.kind]
90
+ return f"https://spec.openapis.org/oas/v{self.version}#{anchor}"
91
+
92
+
93
+ class InvalidSchema(SchemathesisError):
94
+ """Indicates errors in API schema validation or processing."""
95
+
96
+ def __init__(
97
+ self,
98
+ message: str,
99
+ path: str | None = None,
100
+ method: str | None = None,
101
+ ) -> None:
102
+ self.message = message
103
+ self.path = path
104
+ self.method = method
105
+
106
+ @classmethod
107
+ def from_bundle_error(cls, error: BundleError, location: str, name: str | None = None) -> InvalidSchema:
108
+ if location == "body":
109
+ message = f"Can not generate data for {location}! {error}"
110
+ else:
111
+ message = f"Can not generate data for {location} parameter `{name}`! {error}"
112
+ return InvalidSchema(message)
113
+
114
+ @classmethod
115
+ def from_jsonschema_error(
116
+ cls,
117
+ error: ValidationError | JsonSchemaError,
118
+ path: str | None,
119
+ method: str | None,
120
+ config: OutputConfig,
121
+ location: SchemaLocation | None = None,
122
+ ) -> InvalidSchema:
123
+ if location is not None:
124
+ message = location.message
125
+ elif error.absolute_path:
126
+ part = error.absolute_path[-1]
127
+ if isinstance(part, int) and len(error.absolute_path) > 1:
128
+ parent = error.absolute_path[-2]
129
+ message = f"Invalid definition for element at index {part} in `{parent}`"
130
+ else:
131
+ message = f"Invalid `{part}` definition"
132
+ else:
133
+ message = "Invalid schema definition"
134
+ error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
135
+ message += f"\n\nLocation:\n {error_path}"
136
+ instance = truncate_json(error.instance, config=config)
137
+ message += f"\n\nProblematic definition:\n{indent(instance, ' ')}"
138
+ message += "\n\nError details:\n "
139
+ # This default message contains the instance which we already printed
140
+ if "is not valid under any of the given schemas" in error.message:
141
+ message += "The provided definition doesn't match any of the expected formats or types."
142
+ else:
143
+ message += error.message
144
+ message += "\n\n"
145
+ if location is not None:
146
+ message += f"See: {location.specification_url}"
147
+ else:
148
+ message += SCHEMA_ERROR_SUGGESTION
149
+ return cls(message, path=path, method=method)
150
+
151
+ @classmethod
152
+ def from_reference_resolution_error(
153
+ cls, error: RefResolutionError, path: str | None, method: str | None
154
+ ) -> InvalidSchema:
155
+ notes = getattr(error, "__notes__", [])
156
+ # Some exceptions don't have the actual reference in them, hence we add it manually via notes
157
+ reference = str(notes[0])
158
+ message = "Unresolvable reference in the schema"
159
+ # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
160
+ message += f"\n\nError details:\n Reference: {reference}"
161
+ if not reference.startswith(("http://", "https://", "#/")):
162
+ message += "\n File reference could not be resolved. Check that the file exists."
163
+ elif reference.startswith(("#/components", "#/definitions")):
164
+ message += "\n Component does not exist in the schema."
165
+ elif isinstance(error.__cause__, RemoteDocumentError):
166
+ message += f"\n {error.__cause__}"
167
+ return cls(message, path=path, method=method)
168
+
169
+ def as_failing_test_function(self) -> Callable:
170
+ """Create a test function that will fail.
171
+
172
+ This approach allows us to use default pytest reporting style for operation-level schema errors.
173
+ """
174
+
175
+ def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
176
+ __tracebackhide__ = True
177
+ raise self
178
+
179
+ return actual_test
180
+
181
+
182
+ class RemoteDocumentError(SchemathesisError):
183
+ """Remote reference resolution failed.
184
+
185
+ This exception carries more context than the default one in `jsonschema`.
186
+ """
187
+
188
+
189
+ class HookError(SchemathesisError):
190
+ """Happens during hooks loading."""
191
+
192
+ module_path: str
193
+
194
+ __slots__ = ("module_path",)
195
+
196
+ def __init__(self, module_path: str) -> None:
197
+ self.module_path = module_path
198
+
199
+ def __str__(self) -> str:
200
+ return f"Failed to load Schemathesis extensions from `{self.module_path}`"
201
+
202
+
203
+ class InvalidRegexType(InvalidSchema):
204
+ """Raised when an invalid type is used where a regex pattern is expected."""
205
+
206
+
207
+ class InvalidStateMachine(SchemathesisError):
208
+ """Collection of validation errors found in API state machine transitions.
209
+
210
+ Raised during schema initialization when one or more transitions
211
+ contain invalid definitions, such as references to non-existent parameters
212
+ or operations.
213
+ """
214
+
215
+ errors: list[InvalidTransition]
216
+
217
+ __slots__ = ("errors",)
218
+
219
+ def __init__(self, errors: list[InvalidTransition]) -> None:
220
+ self.errors = errors
221
+
222
+ def __str__(self) -> str:
223
+ """Format state machine validation errors in a clear, hierarchical structure."""
224
+ result = "The following API operations contain invalid link definitions:"
225
+
226
+ # Group transitions by source operation, then by target and status
227
+ by_source: dict[str, dict[tuple[str, str], list[InvalidTransition]]] = {}
228
+ for transition in self.errors:
229
+ source_group = by_source.setdefault(transition.source, {})
230
+ target_key = (transition.target, transition.status_code)
231
+ source_group.setdefault(target_key, []).append(transition)
232
+
233
+ for source, target_groups in by_source.items():
234
+ for (target, status), transitions in target_groups.items():
235
+ for transition in transitions:
236
+ result += f"\n\n {format_transition(source, status, transition.name, target)}\n"
237
+ for error in transition.errors:
238
+ result += f"\n - {error.message}"
239
+ return result
240
+
241
+
242
+ def format_transition(source: str, status: str, transition: str, target: str) -> str:
243
+ return f"{source} -> [{status}] {transition} -> {target}"
244
+
245
+
246
+ class InvalidTransition(SchemathesisError):
247
+ """Raised when a stateful transition contains one or more errors."""
248
+
249
+ name: str
250
+ source: str
251
+ target: str
252
+ status_code: str
253
+ errors: list[TransitionValidationError]
254
+
255
+ __slots__ = ("name", "source", "target", "status_code", "errors")
256
+
257
+ def __init__(
258
+ self,
259
+ name: str,
260
+ source: str,
261
+ target: str,
262
+ status_code: str,
263
+ errors: list[TransitionValidationError],
264
+ ) -> None:
265
+ self.name = name
266
+ self.source = source
267
+ self.target = target
268
+ self.status_code = status_code
269
+ self.errors = errors
270
+
271
+
272
+ class TransitionValidationError(SchemathesisError):
273
+ """Single validation error found during stateful transition validation."""
274
+
275
+ message: str
276
+
277
+ __slots__ = ("message",)
278
+
279
+ def __init__(self, message: str) -> None:
280
+ self.message = message
281
+
282
+
283
+ class MalformedMediaType(ValueError):
284
+ """Raised on parsing of incorrect media type."""
285
+
286
+
287
+ class InvalidRegexPattern(InvalidSchema):
288
+ """Raised when a string pattern is not a valid regular expression."""
289
+
290
+ @classmethod
291
+ def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegexPattern:
292
+ match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
293
+ if match:
294
+ message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
295
+ return cls(message)
296
+
297
+ @classmethod
298
+ def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegexPattern:
299
+ if from_examples:
300
+ message = (
301
+ "Failed to generate test cases from examples for this API operation because of "
302
+ f"unsupported regular expression `{error.instance}`"
303
+ )
304
+ else:
305
+ message = (
306
+ "Failed to generate test cases for this API operation because of "
307
+ f"unsupported regular expression `{error.instance}`"
308
+ )
309
+ return cls(message)
310
+
311
+
312
+ class InvalidHeadersExample(InvalidSchema):
313
+ @classmethod
314
+ def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
315
+ message = (
316
+ "Failed to generate test cases from examples for this API operation because of "
317
+ "some header examples are invalid:\n"
318
+ )
319
+ for key, value in headers.items():
320
+ message += f"\n - {key!r}={value!r}"
321
+ message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
322
+ return cls(message)
323
+
324
+
325
+ class IncorrectUsage(SchemathesisError):
326
+ """Indicates incorrect usage of Schemathesis' public API."""
327
+
328
+
329
+ class AuthenticationError(SchemathesisError):
330
+ """Error during authentication provider execution.
331
+
332
+ This error wraps exceptions that occur when obtaining or setting
333
+ authentication data via custom auth providers.
334
+ """
335
+
336
+ def __init__(self, provider_name: str, method: str, message: str) -> None:
337
+ self.provider_name = provider_name
338
+ self.method = method
339
+ self.message = message
340
+ super().__init__(
341
+ f"Error in '{provider_name}.{method}()': {message}\n\n"
342
+ f"Common causes:\n"
343
+ f" - Auth endpoint returned an error response\n"
344
+ f" - Response format doesn't match expectations (text vs JSON)\n"
345
+ f" - Network or connection issues\n"
346
+ f" - Logic error in the authentication provider implementation"
347
+ )
348
+
349
+
350
+ class NoLinksFound(IncorrectUsage):
351
+ """Raised when no valid links are available for stateful testing."""
352
+
353
+
354
+ class InvalidRateLimit(IncorrectUsage):
355
+ """Incorrect input for rate limiting."""
356
+
357
+ def __init__(self, value: str) -> None:
358
+ super().__init__(
359
+ f"Invalid rate limit value: `{value}`. Should be in form `limit/interval`. "
360
+ "Example: `10/m` for 10 requests per minute."
361
+ )
362
+
363
+
364
+ class InternalError(SchemathesisError):
365
+ """Internal error in Schemathesis."""
366
+
367
+
368
+ class SerializationError(SchemathesisError):
369
+ """Can't serialize request payload."""
370
+
371
+
372
+ NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
373
+ UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
374
+ "Unbound prefix: `{prefix}`. "
375
+ "You need to define this namespace in your API schema via the `xml.namespace` keyword. "
376
+ f"See more at {NAMESPACE_DEFINITION_URL}"
377
+ )
378
+
379
+
380
+ class UnboundPrefix(SerializationError):
381
+ """XML serialization error.
382
+
383
+ It happens when the schema does not define a namespace that is used by some of its parts.
384
+ """
385
+
386
+ def __init__(self, prefix: str):
387
+ super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
388
+
389
+
390
+ class UnresolvableReference(SchemathesisError):
391
+ """A reference cannot be resolved."""
392
+
393
+ def __init__(self, reference: str) -> None:
394
+ self.reference = reference
395
+
396
+ def __str__(self) -> str:
397
+ return f"Reference `{self.reference}` cannot be resolved"
398
+
399
+
400
+ class InfiniteRecursiveReference(SchemathesisError):
401
+ """A schema has required references forming an infinite cycle."""
402
+
403
+ def __init__(self, reference: str, cycle: list[str]) -> None:
404
+ self.reference = reference
405
+ self.cycle = cycle
406
+
407
+ def __str__(self) -> str:
408
+ if len(self.cycle) == 1:
409
+ return f"Schema `{self.reference}` has a required reference to itself"
410
+ cycle_str = " ->\n ".join(self.cycle + [self.cycle[0]])
411
+ return f"Schema `{self.reference}` has required references forming a cycle:\n\n {cycle_str}"
412
+
413
+
414
+ class SerializationNotPossible(SerializationError):
415
+ """Not possible to serialize data to specified media type(s).
416
+
417
+ This error occurs in two scenarios:
418
+ 1. When attempting to serialize to a specific media type that isn't supported
419
+ 2. When none of the available media types can be used for serialization
420
+ """
421
+
422
+ def __init__(self, message: str, media_types: list[str]) -> None:
423
+ self.message = message
424
+ self.media_types = media_types
425
+
426
+ def __str__(self) -> str:
427
+ return self.message
428
+
429
+ @classmethod
430
+ def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
431
+ """Create error when no available media type can be used."""
432
+ return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
433
+
434
+ @classmethod
435
+ def for_media_type(cls, media_type: str) -> SerializationNotPossible:
436
+ """Create error when a specific required media type isn't supported."""
437
+ return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
438
+
439
+
440
+ class OperationNotFound(LookupError, SchemathesisError):
441
+ """Raised when an API operation cannot be found in the schema.
442
+
443
+ This error typically occurs during schema access in user code when trying to
444
+ reference a non-existent operation.
445
+ """
446
+
447
+ def __init__(self, message: str, item: str) -> None:
448
+ self.message = message
449
+ self.item = item
450
+
451
+ def __str__(self) -> str:
452
+ return self.message
453
+
454
+
455
+ @enum.unique
456
+ class LoaderErrorKind(str, enum.Enum):
457
+ # Connection related issues
458
+ CONNECTION_SSL = "connection_ssl"
459
+ CONNECTION_OTHER = "connection_other"
460
+ NETWORK_OTHER = "network_other"
461
+ INVALID_CERTIFICATE = "invalid_certificate"
462
+
463
+ # HTTP error codes
464
+ HTTP_SERVER_ERROR = "http_server_error"
465
+ HTTP_CLIENT_ERROR = "http_client_error"
466
+ HTTP_NOT_FOUND = "http_not_found"
467
+ HTTP_FORBIDDEN = "http_forbidden"
468
+
469
+ # Content decoding issues
470
+ SYNTAX_ERROR = "syntax_error"
471
+ UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
472
+ YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
473
+ YAML_NON_STRING_KEYS = "yaml_non_string_keys"
474
+
475
+ # Open API validation
476
+ OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
477
+ OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
478
+ OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
479
+
480
+ # GraphQL validation
481
+ GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
482
+
483
+ # Unclassified
484
+ UNCLASSIFIED = "unclassified"
485
+
486
+
487
+ class LoaderError(SchemathesisError):
488
+ """Failed to load an API schema."""
489
+
490
+ def __init__(
491
+ self,
492
+ kind: LoaderErrorKind,
493
+ message: str,
494
+ url: str | None = None,
495
+ extras: list[str] | None = None,
496
+ ) -> None:
497
+ self.kind = kind
498
+ self.message = message
499
+ self.url = url
500
+ self.extras = extras or []
501
+
502
+ def __str__(self) -> str:
503
+ return self.message
504
+
505
+
506
+ def get_request_error_extras(exc: RequestException) -> list[str]:
507
+ """Extract additional context from a request exception."""
508
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
509
+ from urllib3.exceptions import MaxRetryError
510
+
511
+ if isinstance(exc, SSLError):
512
+ reason = str(exc.args[0].reason)
513
+ return [_remove_ssl_line_number(reason).strip()]
514
+ if isinstance(exc, ConnectionError):
515
+ inner = exc.args[0]
516
+ if isinstance(inner, MaxRetryError) and inner.reason is not None:
517
+ arg = inner.reason.args[0]
518
+ if isinstance(arg, str):
519
+ if ":" not in arg:
520
+ reason = arg
521
+ else:
522
+ _, reason = arg.split(":", maxsplit=1)
523
+ else:
524
+ reason = f"Max retries exceeded with url: {inner.url}"
525
+ return [reason.strip()]
526
+ return [" ".join(map(_clean_inner_request_message, inner.args))]
527
+ if isinstance(exc, ChunkedEncodingError):
528
+ args = exc.args[0].args
529
+ if len(args) == 1:
530
+ return [str(args[0])]
531
+ return [str(args[1])]
532
+ return []
533
+
534
+
535
+ def _remove_ssl_line_number(text: str) -> str:
536
+ return re.sub(r"\(_ssl\.c:\d+\)", "", text)
537
+
538
+
539
+ def _clean_inner_request_message(message: object) -> str:
540
+ if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
541
+ return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
542
+ return str(message)
543
+
544
+
545
+ def get_request_error_message(exc: RequestException) -> str:
546
+ """Extract user-facing message from a request exception."""
547
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, ReadTimeout, SSLError
548
+
549
+ if isinstance(exc, ReadTimeout):
550
+ _, duration = exc.args[0].args[0][:-1].split("read timeout=")
551
+ return f"Read timed out after {duration} seconds"
552
+ if isinstance(exc, SSLError):
553
+ return "SSL verification problem"
554
+ if isinstance(exc, ConnectionError):
555
+ return "Connection failed"
556
+ if isinstance(exc, ChunkedEncodingError):
557
+ return "Connection broken. The server declared chunked encoding but sent an invalid chunk"
558
+ return str(exc)
559
+
560
+
561
+ def split_traceback(traceback: str) -> list[str]:
562
+ return [entry for entry in traceback.splitlines() if entry]
563
+
564
+
565
+ def format_exception(
566
+ error: BaseException,
567
+ *,
568
+ with_traceback: bool = False,
569
+ skip_frames: int = 0,
570
+ ) -> str:
571
+ """Format exception with optional traceback."""
572
+ if not with_traceback:
573
+ lines = traceback.format_exception_only(type(error), error)
574
+ return "".join(lines).strip()
575
+
576
+ trace = error.__traceback__
577
+ if skip_frames > 0:
578
+ trace = extract_nth_traceback(trace, skip_frames)
579
+ lines = traceback.format_exception(type(error), error, trace)
580
+ return "".join(lines).strip()
581
+
582
+
583
+ def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
584
+ depth = 0
585
+ while depth < n and trace is not None:
586
+ trace = trace.tb_next
587
+ depth += 1
588
+ return trace