schemathesis 3.25.5__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 -1766
  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/{cli → engine/phases}/probes.py +63 -70
  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 +153 -39
  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 +483 -367
  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.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.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 -55
  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 -765
  156. schemathesis/cli/output/short.py +0 -40
  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 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  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 -315
  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 -184
  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.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,560 +0,0 @@
1
- from __future__ import annotations
2
- import enum
3
- import json
4
- import re
5
- import traceback
6
- from dataclasses import dataclass, field
7
- from hashlib import sha1
8
- from json import JSONDecodeError
9
- from types import TracebackType
10
- from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
11
-
12
- from .constants import SERIALIZERS_SUGGESTION_MESSAGE
13
- from .failures import FailureContext
14
-
15
- if TYPE_CHECKING:
16
- import hypothesis.errors
17
- from jsonschema import RefResolutionError, ValidationError, SchemaError as JsonSchemaError
18
- from .transports.responses import GenericResponse
19
- from graphql.error import GraphQLFormattedError
20
- from requests import RequestException
21
-
22
-
23
- class CheckFailed(AssertionError):
24
- """Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
25
-
26
- __module__ = "builtins"
27
- context: FailureContext | None
28
- causes: tuple[CheckFailed | AssertionError, ...] | None
29
-
30
- def __init__(
31
- self,
32
- *args: Any,
33
- context: FailureContext | None = None,
34
- causes: tuple[CheckFailed | AssertionError, ...] | None = None,
35
- ):
36
- super().__init__(*args)
37
- self.context = context
38
- self.causes = causes
39
-
40
-
41
- def make_unique_by_key(
42
- check_name: str, check_message: str | None, context: FailureContext | None
43
- ) -> tuple[str | None, ...]:
44
- """A key to distinguish different failed checks.
45
-
46
- It is not only based on `FailureContext`, because the end-user may raise plain `AssertionError` in their custom
47
- checks, and those won't have any context attached.
48
- """
49
- if context is not None:
50
- return context.unique_by_key(check_message)
51
- return check_name, check_message
52
-
53
-
54
- def deduplicate_failed_checks(
55
- checks: list[CheckFailed | AssertionError],
56
- ) -> Generator[CheckFailed | AssertionError, None, None]:
57
- """Keep only unique failed checks."""
58
- seen = set()
59
- for check in checks:
60
- check_message = check.args[0]
61
- if isinstance(check, CheckFailed) and check.context is not None:
62
- key = check.context.unique_by_key(check_message)
63
- else:
64
- key = check_message
65
- if key not in seen:
66
- yield check
67
- seen.add(key)
68
-
69
-
70
- CACHE: dict[str | int, type[CheckFailed]] = {}
71
-
72
-
73
- def get_exception(name: str) -> type[CheckFailed]:
74
- """Create a new exception class with provided name or fetch one from the cache."""
75
- if name in CACHE:
76
- exception_class = CACHE[name]
77
- else:
78
- exception_class = type(name, (CheckFailed,), {})
79
- exception_class.__qualname__ = CheckFailed.__name__
80
- exception_class.__name__ = CheckFailed.__name__
81
- CACHE[name] = exception_class
82
- return exception_class
83
-
84
-
85
- def _get_hashed_exception(prefix: str, message: str) -> type[CheckFailed]:
86
- """Give different exceptions for different error messages."""
87
- messages_digest = sha1(message.encode("utf-8")).hexdigest()
88
- name = f"{prefix}{messages_digest}"
89
- return get_exception(name)
90
-
91
-
92
- def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[CheckFailed]:
93
- # The prefix is needed to distinguish multiple operations with the same error messages
94
- # that are coming from different operations
95
- messages = [exception.args[0] for exception in exceptions]
96
- message = "".join(messages)
97
- return _get_hashed_exception("GroupedException", f"{prefix}{message}")
98
-
99
-
100
- def get_server_error(status_code: int) -> type[CheckFailed]:
101
- """Return new exception for the Internal Server Error cases."""
102
- name = f"ServerError{status_code}"
103
- return get_exception(name)
104
-
105
-
106
- def get_status_code_error(status_code: int) -> type[CheckFailed]:
107
- """Return new exception for an unexpected status code."""
108
- name = f"StatusCodeError{status_code}"
109
- return get_exception(name)
110
-
111
-
112
- def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
113
- """Return new exception for an unexpected response type."""
114
- name = f"SchemaValidationError{expected}_{received}"
115
- return get_exception(name)
116
-
117
-
118
- def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
119
- name = f"MalformedMediaType{media_type}"
120
- return get_exception(name)
121
-
122
-
123
- def get_missing_content_type_error() -> type[CheckFailed]:
124
- """Return new exception for a missing Content-Type header."""
125
- return get_exception("MissingContentTypeError")
126
-
127
-
128
- def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
129
- """Return new exception for schema validation error."""
130
- return _get_hashed_exception("SchemaValidationError", str(exception))
131
-
132
-
133
- def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
134
- """Return new exception for response parsing error."""
135
- return _get_hashed_exception("ResponseParsingError", str(exception))
136
-
137
-
138
- def get_headers_error(message: str) -> type[CheckFailed]:
139
- """Return new exception for missing headers."""
140
- return _get_hashed_exception("MissingHeadersError", message)
141
-
142
-
143
- def get_timeout_error(deadline: float | int) -> type[CheckFailed]:
144
- """Request took too long."""
145
- return _get_hashed_exception("TimeoutError", str(deadline))
146
-
147
-
148
- def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
149
- """When GraphQL response is not a JSON object."""
150
- return get_exception(f"UnexpectedGraphQLResponseError:{type_}")
151
-
152
-
153
- def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[CheckFailed]:
154
- # Canonicalize GraphQL errors by serializing them uniformly and sorting the outcomes
155
- entries = []
156
- for error in errors:
157
- message = error["message"]
158
- if "locations" in error:
159
- message += ";locations:"
160
- for location in sorted(error["locations"]):
161
- message += f"({location['line'],location['column']})"
162
- if "path" in error:
163
- message += ";path:"
164
- for chunk in error["path"]:
165
- message += str(chunk)
166
- entries.append(message)
167
- entries.sort()
168
- return _get_hashed_exception("GraphQLErrors", "".join(entries))
169
-
170
-
171
- SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
172
-
173
-
174
- @dataclass
175
- class OperationSchemaError(Exception):
176
- """Schema associated with an API operation contains an error."""
177
-
178
- __module__ = "builtins"
179
- message: str | None = None
180
- path: str | None = None
181
- method: str | None = None
182
- full_path: str | None = None
183
-
184
- @classmethod
185
- def from_jsonschema_error(
186
- cls, error: ValidationError, path: str | None, method: str | None, full_path: str | None
187
- ) -> OperationSchemaError:
188
- if error.absolute_path:
189
- part = error.absolute_path[-1]
190
- if isinstance(part, int) and len(error.absolute_path) > 1:
191
- parent = error.absolute_path[-2]
192
- message = f"Invalid definition for element at index {part} in `{parent}`"
193
- else:
194
- message = f"Invalid `{part}` definition"
195
- else:
196
- message = "Invalid schema definition"
197
- error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
198
- message += f"\n\nLocation:\n {error_path}"
199
- instance = truncated_json(error.instance)
200
- message += f"\n\nProblematic definition:\n{instance}"
201
- message += "\n\nError details:\n "
202
- # This default message contains the instance which we already printed
203
- if "is not valid under any of the given schemas" in error.message:
204
- message += "The provided definition doesn't match any of the expected formats or types."
205
- else:
206
- message += error.message
207
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
208
- return cls(message, path=path, method=method, full_path=full_path)
209
-
210
- @classmethod
211
- def from_reference_resolution_error(
212
- cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
213
- ) -> OperationSchemaError:
214
- message = "Unresolvable JSON pointer in the schema"
215
- # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
216
- pointer = str(error).split(": ", 1)[-1]
217
- message += f"\n\nError details:\n JSON pointer: {pointer}"
218
- message += "\n This typically means that the schema is referencing a component that doesn't exist."
219
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
220
- return cls(message, path=path, method=method, full_path=full_path)
221
-
222
- def as_failing_test_function(self) -> Callable:
223
- """Create a test function that will fail.
224
-
225
- This approach allows us to use default pytest reporting style for operation-level schema errors.
226
- """
227
-
228
- def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
229
- __tracebackhide__ = True
230
- raise self
231
-
232
- return actual_test
233
-
234
-
235
- @dataclass
236
- class BodyInGetRequestError(OperationSchemaError):
237
- __module__ = "builtins"
238
-
239
-
240
- @dataclass
241
- class OperationNotFound(KeyError):
242
- message: str
243
- item: str
244
- __module__ = "builtins"
245
-
246
- def __str__(self) -> str:
247
- return self.message
248
-
249
-
250
- @dataclass
251
- class InvalidRegularExpression(OperationSchemaError):
252
- is_valid_type: bool = True
253
- __module__ = "builtins"
254
-
255
- @classmethod
256
- def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegularExpression:
257
- match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
258
- if match:
259
- message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
260
- return cls(message)
261
-
262
- @classmethod
263
- def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegularExpression:
264
- if from_examples:
265
- message = (
266
- "Failed to generate test cases from examples for this API operation because of "
267
- f"unsupported regular expression `{error.instance}`"
268
- )
269
- else:
270
- message = (
271
- "Failed to generate test cases for this API operation because of "
272
- f"unsupported regular expression `{error.instance}`"
273
- )
274
- return cls(message)
275
-
276
-
277
- class InvalidHeadersExample(OperationSchemaError):
278
- __module__ = "builtins"
279
-
280
- @classmethod
281
- def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
282
- message = (
283
- "Failed to generate test cases from examples for this API operation because of "
284
- "some header examples are invalid:\n"
285
- )
286
- for key, value in headers.items():
287
- message += f"\n - {key!r}={value!r}"
288
- message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
289
- return cls(message)
290
-
291
-
292
- def truncated_json(data: Any, max_lines: int = 10, max_width: int = 80) -> str:
293
- # Convert JSON to string with indentation
294
- indent = 4
295
- serialized = json.dumps(data, indent=indent)
296
-
297
- # Split string by lines
298
-
299
- lines = [line[: max_width - 3] + "..." if len(line) > max_width else line for line in serialized.split("\n")]
300
-
301
- if len(lines) <= max_lines:
302
- return "\n".join(lines)
303
-
304
- truncated_lines = lines[: max_lines - 1]
305
- indentation = " " * indent
306
- truncated_lines.append(f"{indentation}// Output truncated...")
307
- truncated_lines.append(lines[-1])
308
-
309
- return "\n".join(truncated_lines)
310
-
311
-
312
- MAX_PAYLOAD_SIZE = 512
313
-
314
-
315
- def prepare_response_payload(payload: str, max_size: int = MAX_PAYLOAD_SIZE) -> str:
316
- if payload.endswith("\r\n"):
317
- payload = payload[:-2]
318
- elif payload.endswith("\n"):
319
- payload = payload[:-1]
320
- if len(payload) > max_size:
321
- payload = payload[:max_size] + " // Output truncated..."
322
- return payload
323
-
324
-
325
- class DeadlineExceeded(Exception):
326
- """Test took too long to run."""
327
-
328
- __module__ = "builtins"
329
-
330
- @classmethod
331
- def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
332
- runtime = exc.runtime.total_seconds() * 1000
333
- deadline = exc.deadline.total_seconds() * 1000
334
- return cls(
335
- f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
336
- )
337
-
338
-
339
- @enum.unique
340
- class RuntimeErrorType(str, enum.Enum):
341
- # Connection related issues
342
- CONNECTION_SSL = "connection_ssl"
343
- CONNECTION_OTHER = "connection_other"
344
- NETWORK_OTHER = "network_other"
345
-
346
- # Hypothesis issues
347
- HYPOTHESIS_DEADLINE_EXCEEDED = "hypothesis_deadline_exceeded"
348
- HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
349
- HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
350
- HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
351
- HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
352
- HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
353
- HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
354
-
355
- SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
356
- SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
357
- SCHEMA_GENERIC = "schema_generic"
358
-
359
- SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
360
- SERIALIZATION_UNBOUNDED_PREFIX = "serialization_unbounded_prefix"
361
-
362
- # Unclassified
363
- UNCLASSIFIED = "unclassified"
364
-
365
- @property
366
- def has_useful_traceback(self) -> bool:
367
- return self not in (
368
- RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST,
369
- RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION,
370
- RuntimeErrorType.SCHEMA_GENERIC,
371
- RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
372
- )
373
-
374
-
375
- @enum.unique
376
- class SchemaErrorType(str, enum.Enum):
377
- # Connection related issues
378
- CONNECTION_SSL = "connection_ssl"
379
- CONNECTION_OTHER = "connection_other"
380
- NETWORK_OTHER = "network_other"
381
-
382
- # HTTP error codes
383
- HTTP_SERVER_ERROR = "http_server_error"
384
- HTTP_CLIENT_ERROR = "http_client_error"
385
- HTTP_NOT_FOUND = "http_not_found"
386
- HTTP_FORBIDDEN = "http_forbidden"
387
-
388
- # Content decoding issues
389
- SYNTAX_ERROR = "syntax_error"
390
- UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
391
- YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
392
- YAML_NON_STRING_KEYS = "yaml_non_string_keys"
393
-
394
- # Open API validation
395
- OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
396
- OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
397
- OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
398
- OPEN_API_EXPERIMENTAL_VERSION = "open_api_experimental_version"
399
-
400
- # GraphQL validation
401
- GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
402
-
403
- # Unclassified
404
- UNCLASSIFIED = "unclassified"
405
-
406
-
407
- @dataclass
408
- class SchemaError(RuntimeError):
409
- """Failed to load an API schema."""
410
-
411
- type: SchemaErrorType
412
- message: str
413
- url: str | None = None
414
- response: GenericResponse | None = None
415
- extras: list[str] = field(default_factory=list)
416
-
417
- def __str__(self) -> str:
418
- return self.message
419
-
420
-
421
- class NonCheckError(Exception):
422
- """An error happened in side the runner, but is not related to failed checks.
423
-
424
- Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
425
- on our side.
426
- """
427
-
428
- __module__ = "builtins"
429
-
430
-
431
- class InternalError(Exception):
432
- """Internal error in Schemathesis."""
433
-
434
- __module__ = "builtins"
435
-
436
-
437
- class SkipTest(BaseException):
438
- """Raises when a test should be skipped and return control to the execution engine (own Schemathesis' or pytest)."""
439
-
440
- __module__ = "builtins"
441
-
442
-
443
- SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
444
- f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
445
- )
446
- NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
447
- UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
448
- "Unbound prefix: `{prefix}`. "
449
- "You need to define this namespace in your API schema via the `xml.namespace` keyword. "
450
- f"See more at {NAMESPACE_DEFINITION_URL}"
451
- )
452
-
453
- SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
454
- f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
455
- )
456
-
457
-
458
- class SerializationError(Exception):
459
- """Serialization can not be done."""
460
-
461
- __module__ = "builtins"
462
-
463
-
464
- class UnboundPrefixError(SerializationError):
465
- """XML serialization error.
466
-
467
- It happens when the schema does not define a namespace that is used by some of its parts.
468
- """
469
-
470
- def __init__(self, prefix: str):
471
- super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
472
-
473
-
474
- @dataclass
475
- class SerializationNotPossible(SerializationError):
476
- """Not possible to serialize to any of the media types defined for some API operation.
477
-
478
- Usually, there is still `application/json` along with less common ones, but this error happens when there is no
479
- media type that Schemathesis knows how to serialize data to.
480
- """
481
-
482
- message: str
483
- media_types: list[str]
484
-
485
- __module__ = "builtins"
486
-
487
- def __str__(self) -> str:
488
- return self.message
489
-
490
- @classmethod
491
- def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
492
- return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
493
-
494
- @classmethod
495
- def for_media_type(cls, media_type: str) -> SerializationNotPossible:
496
- return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
497
-
498
-
499
- class UsageError(Exception):
500
- """Incorrect usage of Schemathesis functions."""
501
-
502
-
503
- def maybe_set_assertion_message(exc: AssertionError, check_name: str) -> str:
504
- message = str(exc)
505
- title = f"Custom check failed: `{check_name}`"
506
- if not message:
507
- exc.args = (title, None)
508
- else:
509
- exc.args = (title, message)
510
- return message
511
-
512
-
513
- def format_exception(error: Exception, include_traceback: bool = False) -> str:
514
- """Format exception as text."""
515
- error_type = type(error)
516
- if include_traceback:
517
- lines = traceback.format_exception(error_type, error, error.__traceback__)
518
- else:
519
- lines = traceback.format_exception_only(error_type, error)
520
- return "".join(lines).strip()
521
-
522
-
523
- def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
524
- depth = 0
525
- while depth < n and trace is not None:
526
- trace = trace.tb_next
527
- depth += 1
528
- return trace
529
-
530
-
531
- def remove_ssl_line_number(text: str) -> str:
532
- return re.sub(r"\(_ssl\.c:\d+\)", "", text)
533
-
534
-
535
- def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
536
- from requests.exceptions import SSLError, ConnectionError, ChunkedEncodingError
537
- from urllib3.exceptions import MaxRetryError
538
-
539
- if isinstance(exc, SSLError):
540
- message = "SSL verification problem"
541
- reason = str(exc.args[0].reason)
542
- extra = [remove_ssl_line_number(reason).strip()]
543
- elif isinstance(exc, ConnectionError):
544
- message = "Connection failed"
545
- inner = exc.args[0]
546
- if isinstance(inner, MaxRetryError) and inner.reason is not None:
547
- if ":" not in inner.reason.args[0]:
548
- reason = inner.reason.args[0]
549
- else:
550
- _, reason = inner.reason.args[0].split(":", maxsplit=1)
551
- extra = [reason.strip()]
552
- else:
553
- extra = [" ".join(map(str, inner.args))]
554
- elif isinstance(exc, ChunkedEncodingError):
555
- message = "Connection broken. The server declared chunked encoding but sent an invalid chunk"
556
- extra = [str(exc.args[0].args[1])]
557
- else:
558
- message = str(exc)
559
- extra = []
560
- return message, extra
@@ -1,27 +0,0 @@
1
- from __future__ import annotations
2
- import asyncio
3
-
4
- from aiohttp import web
5
-
6
- from . import _server
7
-
8
-
9
- def _run_server(app: web.Application, port: int) -> None:
10
- """Run the given app on the given port.
11
-
12
- Intended to be called as a target for a separate thread.
13
- NOTE. `aiohttp.web.run_app` works only in the main thread and can't be used here (or maybe can we some tuning)
14
- """
15
- # Set a loop for a new thread (there is no by default for non-main threads)
16
- loop = asyncio.new_event_loop()
17
- asyncio.set_event_loop(loop)
18
- runner = web.AppRunner(app)
19
- loop.run_until_complete(runner.setup())
20
- site = web.TCPSite(runner, "127.0.0.1", port)
21
- loop.run_until_complete(site.start())
22
- loop.run_forever()
23
-
24
-
25
- def run_server(app: web.Application, port: int | None = None, timeout: float = 0.05) -> int:
26
- """Start a thread with the given aiohttp application."""
27
- return _server.run(_run_server, app=app, port=port, timeout=timeout)
@@ -1,10 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from flask import Flask
4
-
5
- from . import _server
6
-
7
-
8
- def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
9
- """Start a thread with the given aiohttp application."""
10
- return _server.run(app.run, port=port, timeout=timeout)
@@ -1,17 +0,0 @@
1
- from __future__ import annotations
2
- import threading
3
- from time import sleep
4
- from typing import Any, Callable
5
-
6
- from aiohttp.test_utils import unused_port
7
-
8
-
9
- def run(target: Callable, port: int | None = None, timeout: float = 0.05, **kwargs: Any) -> int:
10
- """Start a thread with the given aiohttp application."""
11
- if port is None:
12
- port = unused_port()
13
- server_thread = threading.Thread(target=target, kwargs={"port": port, **kwargs})
14
- server_thread.daemon = True
15
- server_thread.start()
16
- sleep(timeout)
17
- return port