schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +233 -307
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -717
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,571 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import re
5
- import traceback
6
- from dataclasses import dataclass, field
7
- from hashlib import sha1
8
- from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
9
-
10
- from .constants import SERIALIZERS_SUGGESTION_MESSAGE
11
- from .internal.output import truncate_json
12
-
13
- if TYPE_CHECKING:
14
- from json import JSONDecodeError
15
- from types import TracebackType
16
-
17
- import hypothesis.errors
18
- from graphql.error import GraphQLFormattedError
19
- from jsonschema import RefResolutionError, ValidationError
20
- from jsonschema import SchemaError as JsonSchemaError
21
- from requests import RequestException
22
-
23
- from .failures import FailureContext
24
- from .transports.responses import GenericResponse
25
-
26
-
27
- class CheckFailed(AssertionError):
28
- """Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
29
-
30
- __module__ = "builtins"
31
- context: FailureContext | None
32
- causes: tuple[CheckFailed | AssertionError, ...] | None
33
-
34
- def __init__(
35
- self,
36
- *args: Any,
37
- context: FailureContext | None = None,
38
- causes: tuple[CheckFailed | AssertionError, ...] | None = None,
39
- ):
40
- super().__init__(*args)
41
- self.context = context
42
- self.causes = causes
43
-
44
-
45
- def make_unique_by_key(
46
- check_name: str, check_message: str | None, context: FailureContext | None
47
- ) -> tuple[str | None, ...]:
48
- """A key to distinguish different failed checks.
49
-
50
- It is not only based on `FailureContext`, because the end-user may raise plain `AssertionError` in their custom
51
- checks, and those won't have any context attached.
52
- """
53
- if context is not None:
54
- return context.unique_by_key(check_message)
55
- return check_name, check_message
56
-
57
-
58
- def deduplicate_failed_checks(
59
- checks: list[CheckFailed | AssertionError],
60
- ) -> Generator[CheckFailed | AssertionError, None, None]:
61
- """Keep only unique failed checks."""
62
- seen = set()
63
- for check in checks:
64
- check_message = check.args[0]
65
- if isinstance(check, CheckFailed) and check.context is not None:
66
- key = check.context.unique_by_key(check_message)
67
- else:
68
- key = check_message
69
- if key not in seen:
70
- yield check
71
- seen.add(key)
72
-
73
-
74
- CACHE: dict[str | int, type[CheckFailed]] = {}
75
-
76
-
77
- def get_exception(name: str) -> type[CheckFailed]:
78
- """Create a new exception class with provided name or fetch one from the cache."""
79
- if name in CACHE:
80
- exception_class = CACHE[name]
81
- else:
82
- exception_class = type(name, (CheckFailed,), {})
83
- exception_class.__qualname__ = CheckFailed.__name__
84
- exception_class.__name__ = CheckFailed.__name__
85
- CACHE[name] = exception_class
86
- return exception_class
87
-
88
-
89
- def _get_hashed_exception(prefix: str, message: str) -> type[CheckFailed]:
90
- """Give different exceptions for different error messages."""
91
- messages_digest = sha1(message.encode("utf-8")).hexdigest()
92
- name = f"{prefix}{messages_digest}"
93
- return get_exception(name)
94
-
95
-
96
- def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[CheckFailed]:
97
- # The prefix is needed to distinguish multiple operations with the same error messages
98
- # that are coming from different operations
99
- messages = [exception.args[0] for exception in exceptions]
100
- message = "".join(messages)
101
- return _get_hashed_exception("GroupedException", f"{prefix}{message}")
102
-
103
-
104
- def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
105
- """Return new exception for the Internal Server Error cases."""
106
- name = f"ServerError{prefix}{status_code}"
107
- return get_exception(name)
108
-
109
-
110
- def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
111
- """Return new exception for an unexpected status code."""
112
- name = f"StatusCodeError{prefix}{status_code}"
113
- return get_exception(name)
114
-
115
-
116
- def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
117
- """Return new exception for an unexpected response type."""
118
- name = f"SchemaValidationError{prefix}{expected}_{received}"
119
- return get_exception(name)
120
-
121
-
122
- def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
123
- name = f"MalformedMediaType{prefix}{media_type}"
124
- return get_exception(name)
125
-
126
-
127
- def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
128
- """Return new exception for a missing Content-Type header."""
129
- return get_exception(f"MissingContentTypeError{prefix}")
130
-
131
-
132
- def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
133
- """Return new exception for schema validation error."""
134
- return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
135
-
136
-
137
- def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
138
- """Return new exception for response parsing error."""
139
- return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
140
-
141
-
142
- def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
143
- """Return new exception for missing headers."""
144
- return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
145
-
146
-
147
- def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
148
- return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
149
-
150
-
151
- def get_positive_acceptance_error(prefix: str, status: int) -> type[CheckFailed]:
152
- return _get_hashed_exception(f"RejectedPositiveDataError{prefix}", str(status))
153
-
154
-
155
- def get_use_after_free_error(free: str) -> type[CheckFailed]:
156
- return _get_hashed_exception("UseAfterFreeError", free)
157
-
158
-
159
- def get_ensure_resource_availability_error(operation: str) -> type[CheckFailed]:
160
- return _get_hashed_exception("EnsureResourceAvailabilityError", operation)
161
-
162
-
163
- def get_ignored_auth_error(operation: str) -> type[CheckFailed]:
164
- return _get_hashed_exception("IgnoredAuthError", operation)
165
-
166
-
167
- def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
168
- """Request took too long."""
169
- return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
170
-
171
-
172
- def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
173
- """When GraphQL response is not a JSON object."""
174
- return get_exception(f"UnexpectedGraphQLResponseError:{type_}")
175
-
176
-
177
- def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[CheckFailed]:
178
- # Canonicalize GraphQL errors by serializing them uniformly and sorting the outcomes
179
- entries = []
180
- for error in errors:
181
- message = error["message"]
182
- if "locations" in error:
183
- message += ";locations:"
184
- for location in sorted(error["locations"]):
185
- message += f"({location['line'], location['column']})"
186
- if "path" in error:
187
- message += ";path:"
188
- for chunk in error["path"]:
189
- message += str(chunk)
190
- entries.append(message)
191
- entries.sort()
192
- return _get_hashed_exception("GraphQLErrors", "".join(entries))
193
-
194
-
195
- SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
196
-
197
-
198
- @dataclass
199
- class OperationSchemaError(Exception):
200
- """Schema associated with an API operation contains an error."""
201
-
202
- __module__ = "builtins"
203
- message: str | None = None
204
- path: str | None = None
205
- method: str | None = None
206
- full_path: str | None = None
207
-
208
- @classmethod
209
- def from_jsonschema_error(
210
- cls, error: ValidationError, path: str | None, method: str | None, full_path: str | None
211
- ) -> OperationSchemaError:
212
- if error.absolute_path:
213
- part = error.absolute_path[-1]
214
- if isinstance(part, int) and len(error.absolute_path) > 1:
215
- parent = error.absolute_path[-2]
216
- message = f"Invalid definition for element at index {part} in `{parent}`"
217
- else:
218
- message = f"Invalid `{part}` definition"
219
- else:
220
- message = "Invalid schema definition"
221
- error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
222
- message += f"\n\nLocation:\n {error_path}"
223
- instance = truncate_json(error.instance)
224
- message += f"\n\nProblematic definition:\n{instance}"
225
- message += "\n\nError details:\n "
226
- # This default message contains the instance which we already printed
227
- if "is not valid under any of the given schemas" in error.message:
228
- message += "The provided definition doesn't match any of the expected formats or types."
229
- else:
230
- message += error.message
231
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
232
- return cls(message, path=path, method=method, full_path=full_path)
233
-
234
- @classmethod
235
- def from_reference_resolution_error(
236
- cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
237
- ) -> OperationSchemaError:
238
- notes = getattr(error, "__notes__", [])
239
- # Some exceptions don't have the actual reference in them, hence we add it manually via notes
240
- pointer = f"'{notes[0]}'"
241
- message = "Unresolvable JSON pointer in the schema"
242
- # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
243
- message += f"\n\nError details:\n JSON pointer: {pointer}"
244
- message += "\n This typically means that the schema is referencing a component that doesn't exist."
245
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
246
- return cls(message, path=path, method=method, full_path=full_path)
247
-
248
- def as_failing_test_function(self) -> Callable:
249
- """Create a test function that will fail.
250
-
251
- This approach allows us to use default pytest reporting style for operation-level schema errors.
252
- """
253
-
254
- def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
255
- __tracebackhide__ = True
256
- raise self
257
-
258
- return actual_test
259
-
260
-
261
- @dataclass
262
- class BodyInGetRequestError(OperationSchemaError):
263
- __module__ = "builtins"
264
-
265
-
266
- @dataclass
267
- class OperationNotFound(KeyError):
268
- message: str
269
- item: str
270
- __module__ = "builtins"
271
-
272
- def __str__(self) -> str:
273
- return self.message
274
-
275
-
276
- @dataclass
277
- class InvalidRegularExpression(OperationSchemaError):
278
- is_valid_type: bool = True
279
- __module__ = "builtins"
280
-
281
- @classmethod
282
- def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegularExpression:
283
- match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
284
- if match:
285
- message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
286
- return cls(message)
287
-
288
- @classmethod
289
- def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegularExpression:
290
- if from_examples:
291
- message = (
292
- "Failed to generate test cases from examples for this API operation because of "
293
- f"unsupported regular expression `{error.instance}`"
294
- )
295
- else:
296
- message = (
297
- "Failed to generate test cases for this API operation because of "
298
- f"unsupported regular expression `{error.instance}`"
299
- )
300
- return cls(message)
301
-
302
-
303
- class InvalidHeadersExample(OperationSchemaError):
304
- __module__ = "builtins"
305
-
306
- @classmethod
307
- def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
308
- message = (
309
- "Failed to generate test cases from examples for this API operation because of "
310
- "some header examples are invalid:\n"
311
- )
312
- for key, value in headers.items():
313
- message += f"\n - {key!r}={value!r}"
314
- message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
315
- return cls(message)
316
-
317
-
318
- class DeadlineExceeded(Exception):
319
- """Test took too long to run."""
320
-
321
- __module__ = "builtins"
322
-
323
- @classmethod
324
- def from_exc(cls, exc: hypothesis.errors.DeadlineExceeded) -> DeadlineExceeded:
325
- runtime = exc.runtime.total_seconds() * 1000
326
- deadline = exc.deadline.total_seconds() * 1000
327
- return cls(
328
- f"Test running time is too slow! It took {runtime:.2f}ms, which exceeds the deadline of {deadline:.2f}ms.\n"
329
- )
330
-
331
-
332
- class RecursiveReferenceError(Exception):
333
- """Recursive reference is impossible to resolve due to current limitations."""
334
-
335
- __module__ = "builtins"
336
-
337
-
338
- @enum.unique
339
- class RuntimeErrorType(str, enum.Enum):
340
- # Connection related issues
341
- CONNECTION_SSL = "connection_ssl"
342
- CONNECTION_OTHER = "connection_other"
343
- NETWORK_OTHER = "network_other"
344
-
345
- # Hypothesis issues
346
- HYPOTHESIS_DEADLINE_EXCEEDED = "hypothesis_deadline_exceeded"
347
- HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
348
- HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
349
- HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE = "hypothesis_health_check_data_too_large"
350
- HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH = "hypothesis_health_check_filter_too_much"
351
- HYPOTHESIS_HEALTH_CHECK_TOO_SLOW = "hypothesis_health_check_too_slow"
352
- HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE = "hypothesis_health_check_large_base_example"
353
-
354
- SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
355
- SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
356
- SCHEMA_UNSUPPORTED = "schema_unsupported"
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_UNSUPPORTED,
371
- RuntimeErrorType.SCHEMA_GENERIC,
372
- RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
373
- )
374
-
375
-
376
- @enum.unique
377
- class SchemaErrorType(str, enum.Enum):
378
- # Connection related issues
379
- CONNECTION_SSL = "connection_ssl"
380
- CONNECTION_OTHER = "connection_other"
381
- NETWORK_OTHER = "network_other"
382
-
383
- # HTTP error codes
384
- HTTP_SERVER_ERROR = "http_server_error"
385
- HTTP_CLIENT_ERROR = "http_client_error"
386
- HTTP_NOT_FOUND = "http_not_found"
387
- HTTP_FORBIDDEN = "http_forbidden"
388
-
389
- # Content decoding issues
390
- SYNTAX_ERROR = "syntax_error"
391
- UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
392
- YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
393
- YAML_NON_STRING_KEYS = "yaml_non_string_keys"
394
-
395
- # Open API validation
396
- OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
397
- OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
398
- OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
399
- OPEN_API_EXPERIMENTAL_VERSION = "open_api_experimental_version"
400
-
401
- # GraphQL validation
402
- GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
403
-
404
- # Unclassified
405
- UNCLASSIFIED = "unclassified"
406
-
407
-
408
- @dataclass
409
- class SchemaError(RuntimeError):
410
- """Failed to load an API schema."""
411
-
412
- type: SchemaErrorType
413
- message: str
414
- url: str | None = None
415
- response: GenericResponse | None = None
416
- extras: list[str] = field(default_factory=list)
417
-
418
- def __str__(self) -> str:
419
- return self.message
420
-
421
-
422
- class NonCheckError(Exception):
423
- """An error happened in side the runner, but is not related to failed checks.
424
-
425
- Used primarily to not let Hypothesis consider the test as flaky or detect multiple failures as we handle it
426
- on our side.
427
- """
428
-
429
- __module__ = "builtins"
430
-
431
-
432
- class InternalError(Exception):
433
- """Internal error in Schemathesis."""
434
-
435
- __module__ = "builtins"
436
-
437
-
438
- class SkipTest(BaseException):
439
- """Raises when a test should be skipped and return control to the execution engine (own Schemathesis' or pytest)."""
440
-
441
- __module__ = "builtins"
442
-
443
-
444
- SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
445
- f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
446
- )
447
- NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
448
- UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
449
- "Unbound prefix: `{prefix}`. "
450
- "You need to define this namespace in your API schema via the `xml.namespace` keyword. "
451
- f"See more at {NAMESPACE_DEFINITION_URL}"
452
- )
453
-
454
- SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
455
- f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
456
- )
457
-
458
-
459
- class SerializationError(Exception):
460
- """Serialization can not be done."""
461
-
462
- __module__ = "builtins"
463
-
464
-
465
- class UnboundPrefixError(SerializationError):
466
- """XML serialization error.
467
-
468
- It happens when the schema does not define a namespace that is used by some of its parts.
469
- """
470
-
471
- def __init__(self, prefix: str):
472
- super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
473
-
474
-
475
- @dataclass
476
- class SerializationNotPossible(SerializationError):
477
- """Not possible to serialize to any of the media types defined for some API operation.
478
-
479
- Usually, there is still `application/json` along with less common ones, but this error happens when there is no
480
- media type that Schemathesis knows how to serialize data to.
481
- """
482
-
483
- message: str
484
- media_types: list[str]
485
-
486
- __module__ = "builtins"
487
-
488
- def __str__(self) -> str:
489
- return self.message
490
-
491
- @classmethod
492
- def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
493
- return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
494
-
495
- @classmethod
496
- def for_media_type(cls, media_type: str) -> SerializationNotPossible:
497
- return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
498
-
499
-
500
- class UsageError(Exception):
501
- """Incorrect usage of Schemathesis functions."""
502
-
503
-
504
- def maybe_set_assertion_message(exc: AssertionError, check_name: str) -> str:
505
- message = str(exc)
506
- title = f"Custom check failed: `{check_name}`"
507
- if not message:
508
- exc.args = (title, None)
509
- else:
510
- exc.args = (title, message)
511
- return message
512
-
513
-
514
- def format_exception(error: Exception, include_traceback: bool = False) -> str:
515
- """Format exception as text."""
516
- error_type = type(error)
517
- if include_traceback:
518
- lines = traceback.format_exception(error_type, error, error.__traceback__)
519
- else:
520
- lines = traceback.format_exception_only(error_type, error)
521
- return "".join(lines).strip()
522
-
523
-
524
- def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
525
- depth = 0
526
- while depth < n and trace is not None:
527
- trace = trace.tb_next
528
- depth += 1
529
- return trace
530
-
531
-
532
- def remove_ssl_line_number(text: str) -> str:
533
- return re.sub(r"\(_ssl\.c:\d+\)", "", text)
534
-
535
-
536
- def _clean_inner_request_message(message: Any) -> str:
537
- if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
538
- return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
539
- return str(message)
540
-
541
-
542
- def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
543
- from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
544
- from urllib3.exceptions import MaxRetryError
545
-
546
- if isinstance(exc, SSLError):
547
- message = "SSL verification problem"
548
- reason = str(exc.args[0].reason)
549
- extra = [remove_ssl_line_number(reason).strip()]
550
- elif isinstance(exc, ConnectionError):
551
- message = "Connection failed"
552
- inner = exc.args[0]
553
- if isinstance(inner, MaxRetryError) and inner.reason is not None:
554
- arg = inner.reason.args[0]
555
- if isinstance(arg, str):
556
- if ":" not in arg:
557
- reason = arg
558
- else:
559
- _, reason = arg.split(":", maxsplit=1)
560
- else:
561
- reason = f"Max retries exceeded with url: {inner.url}"
562
- extra = [reason.strip()]
563
- else:
564
- extra = [" ".join(map(_clean_inner_request_message, inner.args))]
565
- elif isinstance(exc, ChunkedEncodingError):
566
- message = "Connection broken. The server declared chunked encoding but sent an invalid chunk"
567
- extra = [str(exc.args[0].args[1])]
568
- else:
569
- message = str(exc)
570
- extra = []
571
- return message, extra
@@ -1,109 +0,0 @@
1
- import os
2
- from dataclasses import dataclass, field
3
-
4
- from ..constants import TRUE_VALUES
5
-
6
-
7
- @dataclass(eq=False)
8
- class Experiment:
9
- name: str
10
- verbose_name: str
11
- env_var: str
12
- description: str
13
- discussion_url: str
14
- _storage: "ExperimentSet" = field(repr=False)
15
-
16
- def enable(self) -> None:
17
- self._storage.enable(self)
18
-
19
- def disable(self) -> None:
20
- self._storage.disable(self)
21
-
22
- @property
23
- def is_enabled(self) -> bool:
24
- return self._storage.is_enabled(self)
25
-
26
- @property
27
- def is_env_var_set(self) -> bool:
28
- return os.getenv(self.env_var, "").lower() in TRUE_VALUES
29
-
30
-
31
- @dataclass
32
- class ExperimentSet:
33
- available: set = field(default_factory=set)
34
- enabled: set = field(default_factory=set)
35
-
36
- def create_experiment(
37
- self, name: str, verbose_name: str, env_var: str, description: str, discussion_url: str
38
- ) -> Experiment:
39
- instance = Experiment(
40
- name=name,
41
- verbose_name=verbose_name,
42
- env_var=f"{ENV_PREFIX}_{env_var}",
43
- description=description,
44
- discussion_url=discussion_url,
45
- _storage=self,
46
- )
47
- self.available.add(instance)
48
- if instance.is_env_var_set:
49
- self.enable(instance)
50
- return instance
51
-
52
- def enable(self, feature: Experiment) -> None:
53
- self.enabled.add(feature)
54
-
55
- def disable(self, feature: Experiment) -> None:
56
- self.enabled.discard(feature)
57
-
58
- def disable_all(self) -> None:
59
- self.enabled.clear()
60
-
61
- def is_enabled(self, feature: Experiment) -> bool:
62
- return feature in self.enabled
63
-
64
-
65
- ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
66
- GLOBAL_EXPERIMENTS = ExperimentSet()
67
-
68
- OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
69
- name="openapi-3.1",
70
- verbose_name="OpenAPI 3.1",
71
- env_var="OPENAPI_3_1",
72
- description="Support for response validation",
73
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
74
- )
75
- SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
76
- name="schema-analysis",
77
- verbose_name="Schema Analysis",
78
- env_var="SCHEMA_ANALYSIS",
79
- description="Analyzing API schemas via Schemathesis.io",
80
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
81
- )
82
- STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
83
- name="stateful-test-runner",
84
- verbose_name="New Stateful Test Runner",
85
- env_var="STATEFUL_TEST_RUNNER",
86
- description="State machine-based runner for stateful tests in CLI",
87
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
88
- )
89
- STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
90
- name="stateful-only",
91
- verbose_name="Stateful Only",
92
- env_var="STATEFUL_ONLY",
93
- description="Run only stateful tests",
94
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
95
- )
96
- COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
97
- name="coverage-phase",
98
- verbose_name="Coverage phase",
99
- env_var="COVERAGE_PHASE",
100
- description="Generate covering test cases",
101
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
102
- )
103
- POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
104
- name="positive_data_acceptance",
105
- verbose_name="Positive Data Acceptance",
106
- env_var="POSITIVE_DATA_ACCEPTANCE",
107
- description="Verifying schema-conformant data is accepted",
108
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
109
- )