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
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from dataclasses import dataclass
5
+
6
+ SCHEMATHESIS_TEST_CASE_HEADER = "X-Schemathesis-TestCaseId"
7
+ HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER = ":memory:"
8
+ INTERNAL_BUFFER_SIZE = 32 * 1024
9
+ DEFAULT_STATEFUL_STEP_COUNT = 6
10
+
11
+
12
+ class NotSet: ...
13
+
14
+
15
+ NOT_SET = NotSet()
16
+
17
+
18
+ class SpecificationFeature(str, enum.Enum):
19
+ """Features that Schemathesis can provide for different specifications."""
20
+
21
+ STATEFUL_TESTING = "stateful_testing"
22
+ COVERAGE = "coverage_tests"
23
+ EXAMPLES = "example_tests"
24
+
25
+
26
+ @dataclass
27
+ class Specification:
28
+ kind: SpecificationKind
29
+ version: str
30
+
31
+ @classmethod
32
+ def openapi(cls, version: str) -> Specification:
33
+ return cls(kind=SpecificationKind.OPENAPI, version=version)
34
+
35
+ @classmethod
36
+ def graphql(cls, version: str) -> Specification:
37
+ return cls(kind=SpecificationKind.GRAPHQL, version=version)
38
+
39
+ @property
40
+ def name(self) -> str:
41
+ name = {SpecificationKind.GRAPHQL: "GraphQL", SpecificationKind.OPENAPI: "Open API"}[self.kind]
42
+ return f"{name} {self.version}".strip()
43
+
44
+ def supports_feature(self, feature: SpecificationFeature) -> bool:
45
+ """Check if Schemathesis supports a given feature for this specification."""
46
+ if self.kind == SpecificationKind.OPENAPI:
47
+ return feature in {
48
+ SpecificationFeature.STATEFUL_TESTING,
49
+ SpecificationFeature.COVERAGE,
50
+ SpecificationFeature.EXAMPLES,
51
+ }
52
+ return False
53
+
54
+
55
+ class SpecificationKind(str, enum.Enum):
56
+ """Specification of the given schema."""
57
+
58
+ OPENAPI = "openapi"
59
+ GRAPHQL = "graphql"
60
+
61
+
62
+ def string_to_boolean(value: str) -> str | bool:
63
+ if value.lower() in ("y", "yes", "t", "true", "on", "1"):
64
+ return True
65
+ if value.lower() in ("n", "no", "f", "false", "off", "0"):
66
+ return False
67
+ return value
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from jsonschema import RefResolutionError, RefResolver
8
+
9
+ try:
10
+ BaseExceptionGroup = BaseExceptionGroup # type: ignore
11
+ except NameError:
12
+ from exceptiongroup import BaseExceptionGroup # type: ignore
13
+
14
+
15
+ def __getattr__(name: str) -> type[RefResolutionError] | type[RefResolver] | type[BaseExceptionGroup]:
16
+ with warnings.catch_warnings():
17
+ warnings.simplefilter("ignore", DeprecationWarning)
18
+ if name == "RefResolutionError":
19
+ # `jsonschema` is pinned, this warning is not useful for the end user
20
+ from jsonschema import RefResolutionError
21
+
22
+ return RefResolutionError
23
+ if name == "RefResolver":
24
+ from jsonschema import RefResolver
25
+
26
+ return RefResolver
27
+ if name == "BaseExceptionGroup":
28
+ return BaseExceptionGroup
29
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
30
+
31
+
32
+ __all__ = ["BaseExceptionGroup", "RefResolutionError"]
@@ -0,0 +1,2 @@
1
+ class SkipTest(BaseException):
2
+ """Raised when the current test should be skipped and the executor then continues normally."""
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from shlex import quote
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schemathesis.core import SCHEMATHESIS_TEST_CASE_HEADER
8
+
9
+ if TYPE_CHECKING:
10
+ from requests.models import CaseInsensitiveDict
11
+
12
+
13
+ def generate(
14
+ *,
15
+ method: str,
16
+ url: str,
17
+ body: str | bytes | None,
18
+ verify: bool,
19
+ headers: dict[str, Any],
20
+ known_generated_headers: dict[str, Any] | None,
21
+ ) -> str:
22
+ """Generate a code snippet for making HTTP requests."""
23
+ _filter_headers(headers, known_generated_headers or {})
24
+ command = f"curl -X {method}"
25
+ for key, value in headers.items():
26
+ header = f"{key}: {value}"
27
+ command += f" -H {quote(header)}"
28
+ if body:
29
+ if isinstance(body, bytes):
30
+ body = body.decode("utf-8", errors="replace")
31
+ command += f" -d {quote(body)}"
32
+ if not verify:
33
+ command += " --insecure"
34
+ return f"{command} {quote(url)}"
35
+
36
+
37
+ def _filter_headers(headers: dict[str, Any], known_generated_headers: dict[str, Any]) -> None:
38
+ for key in list(headers):
39
+ if key not in known_generated_headers and key in get_excluded_headers():
40
+ del headers[key]
41
+
42
+
43
+ @lru_cache
44
+ def get_excluded_headers() -> CaseInsensitiveDict:
45
+ from requests.structures import CaseInsensitiveDict
46
+ from requests.utils import default_headers
47
+
48
+ # These headers are added automatically by Schemathesis or `requests`.
49
+ # Do not show them in code samples to make them more readable
50
+
51
+ return CaseInsensitiveDict(
52
+ {
53
+ "Content-Length": None,
54
+ "Transfer-Encoding": None,
55
+ SCHEMATHESIS_TEST_CASE_HEADER: None,
56
+ **default_headers(),
57
+ }
58
+ )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import lru_cache
5
+ from typing import TYPE_CHECKING, Any, BinaryIO, TextIO
6
+
7
+ if TYPE_CHECKING:
8
+ import yaml
9
+
10
+
11
+ @lru_cache
12
+ def get_yaml_loader() -> type[yaml.SafeLoader]:
13
+ """Create a YAML loader, that doesn't parse specific tokens into Python objects."""
14
+ import yaml
15
+
16
+ try:
17
+ from yaml import CSafeLoader as SafeLoader
18
+ except ImportError:
19
+ from yaml import SafeLoader # type: ignore
20
+
21
+ cls: type[yaml.SafeLoader] = type("YAMLLoader", (SafeLoader,), {})
22
+ cls.yaml_implicit_resolvers = {
23
+ key: [(tag, regexp) for tag, regexp in mapping if tag != "tag:yaml.org,2002:timestamp"]
24
+ for key, mapping in cls.yaml_implicit_resolvers.copy().items()
25
+ }
26
+
27
+ # Fix pyyaml scientific notation parse bug
28
+ # See PR: https://github.com/yaml/pyyaml/pull/174 for upstream fix
29
+ cls.add_implicit_resolver( # type: ignore
30
+ "tag:yaml.org,2002:float",
31
+ re.compile(
32
+ r"""^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+]?[0-9]+)?
33
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
34
+ |\.[0-9_]+(?:[eE][-+]?[0-9]+)?
35
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
36
+ |[-+]?\.(?:inf|Inf|INF)
37
+ |\.(?:nan|NaN|NAN))$""",
38
+ re.VERBOSE,
39
+ ),
40
+ list("-+0123456789."),
41
+ )
42
+
43
+ def construct_mapping(self: SafeLoader, node: yaml.Node, deep: bool = False) -> dict[str, Any]:
44
+ if isinstance(node, yaml.MappingNode):
45
+ self.flatten_mapping(node) # type: ignore
46
+ mapping = {}
47
+ for key_node, value_node in node.value:
48
+ # If the key has a tag different from `str` - use its string value.
49
+ # With this change all integer keys or YAML 1.1 boolean-ish values like "on" / "off" will not be cast to
50
+ # a different type
51
+ if key_node.tag != "tag:yaml.org,2002:str":
52
+ key = key_node.value
53
+ else:
54
+ key = self.construct_object(key_node, deep) # type: ignore
55
+ mapping[key] = self.construct_object(value_node, deep) # type: ignore
56
+ return mapping
57
+
58
+ cls.construct_mapping = construct_mapping # type: ignore
59
+ return cls
60
+
61
+
62
+ def deserialize_yaml(stream: str | bytes | TextIO | BinaryIO) -> Any:
63
+ import yaml
64
+
65
+ return yaml.load(stream, get_yaml_loader())
@@ -0,0 +1,459 @@
1
+ """Base error handling that is not tied to any specific API specification or execution context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ import re
7
+ import traceback
8
+ from types import TracebackType
9
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn
10
+
11
+ from schemathesis.core.output import truncate_json
12
+
13
+ if TYPE_CHECKING:
14
+ from jsonschema import SchemaError as JsonSchemaError
15
+ from jsonschema import ValidationError
16
+ from requests import RequestException
17
+
18
+ from schemathesis.config import OutputConfig
19
+ from schemathesis.core.compat import RefResolutionError
20
+
21
+
22
+ SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
23
+ SERIALIZERS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/custom-serializers/"
24
+ SERIALIZERS_SUGGESTION_MESSAGE = f"Check your schema or add custom serializers: {SERIALIZERS_DOCUMENTATION_URL}"
25
+ SERIALIZATION_NOT_POSSIBLE_MESSAGE = f"No supported serializers for media types: {{}}\n{SERIALIZERS_SUGGESTION_MESSAGE}"
26
+ SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
27
+ f"Cannot serialize to '{{}}' (unsupported media type)\n{SERIALIZERS_SUGGESTION_MESSAGE}"
28
+ )
29
+ RECURSIVE_REFERENCE_ERROR_MESSAGE = (
30
+ "Currently, Schemathesis can't generate data for this operation due to "
31
+ "recursive references in the operation definition. See more information in "
32
+ "this issue - https://github.com/schemathesis/schemathesis/issues/947"
33
+ )
34
+
35
+
36
+ class SchemathesisError(Exception):
37
+ """Base exception class for all Schemathesis errors."""
38
+
39
+
40
+ class InvalidSchema(SchemathesisError):
41
+ """Indicates errors in API schema validation or processing."""
42
+
43
+ def __init__(
44
+ self,
45
+ message: str,
46
+ path: str | None = None,
47
+ method: str | None = None,
48
+ ) -> None:
49
+ self.message = message
50
+ self.path = path
51
+ self.method = method
52
+
53
+ @classmethod
54
+ def from_jsonschema_error(
55
+ cls, error: ValidationError, path: str | None, method: str | None, config: OutputConfig
56
+ ) -> InvalidSchema:
57
+ if error.absolute_path:
58
+ part = error.absolute_path[-1]
59
+ if isinstance(part, int) and len(error.absolute_path) > 1:
60
+ parent = error.absolute_path[-2]
61
+ message = f"Invalid definition for element at index {part} in `{parent}`"
62
+ else:
63
+ message = f"Invalid `{part}` definition"
64
+ else:
65
+ message = "Invalid schema definition"
66
+ error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
67
+ message += f"\n\nLocation:\n {error_path}"
68
+ instance = truncate_json(error.instance, config=config)
69
+ message += f"\n\nProblematic definition:\n{instance}"
70
+ message += "\n\nError details:\n "
71
+ # This default message contains the instance which we already printed
72
+ if "is not valid under any of the given schemas" in error.message:
73
+ message += "The provided definition doesn't match any of the expected formats or types."
74
+ else:
75
+ message += error.message
76
+ message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
77
+ return cls(message, path=path, method=method)
78
+
79
+ @classmethod
80
+ def from_reference_resolution_error(
81
+ cls, error: RefResolutionError, path: str | None, method: str | None
82
+ ) -> InvalidSchema:
83
+ notes = getattr(error, "__notes__", [])
84
+ # Some exceptions don't have the actual reference in them, hence we add it manually via notes
85
+ pointer = f"'{notes[0]}'"
86
+ message = "Unresolvable JSON pointer in the schema"
87
+ # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
88
+ message += f"\n\nError details:\n JSON pointer: {pointer}"
89
+ message += "\n This typically means that the schema is referencing a component that doesn't exist."
90
+ message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
91
+ return cls(message, path=path, method=method)
92
+
93
+ def as_failing_test_function(self) -> Callable:
94
+ """Create a test function that will fail.
95
+
96
+ This approach allows us to use default pytest reporting style for operation-level schema errors.
97
+ """
98
+
99
+ def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
100
+ __tracebackhide__ = True
101
+ raise self
102
+
103
+ return actual_test
104
+
105
+
106
+ class HookError(SchemathesisError):
107
+ """Happens during hooks loading."""
108
+
109
+ module_path: str
110
+
111
+ __slots__ = ("module_path",)
112
+
113
+ def __init__(self, module_path: str) -> None:
114
+ self.module_path = module_path
115
+
116
+ def __str__(self) -> str:
117
+ return f"Failed to load Schemathesis extensions from `{self.module_path}`"
118
+
119
+
120
+ class InvalidRegexType(InvalidSchema):
121
+ """Raised when an invalid type is used where a regex pattern is expected."""
122
+
123
+
124
+ class InvalidStateMachine(SchemathesisError):
125
+ """Collection of validation errors found in API state machine transitions.
126
+
127
+ Raised during schema initialization when one or more transitions
128
+ contain invalid definitions, such as references to non-existent parameters
129
+ or operations.
130
+ """
131
+
132
+ errors: list[InvalidTransition]
133
+
134
+ __slots__ = ("errors",)
135
+
136
+ def __init__(self, errors: list[InvalidTransition]) -> None:
137
+ self.errors = errors
138
+
139
+ def __str__(self) -> str:
140
+ """Format state machine validation errors in a clear, hierarchical structure."""
141
+ result = "The following API operations contain invalid link definitions:"
142
+
143
+ # Group transitions by source operation, then by target and status
144
+ by_source: dict[str, dict[tuple[str, str], list[InvalidTransition]]] = {}
145
+ for transition in self.errors:
146
+ source_group = by_source.setdefault(transition.source, {})
147
+ target_key = (transition.target, transition.status_code)
148
+ source_group.setdefault(target_key, []).append(transition)
149
+
150
+ for source, target_groups in by_source.items():
151
+ for (target, status), transitions in target_groups.items():
152
+ for transition in transitions:
153
+ result += f"\n\n {_format_transition(source, status, transition.name, target)}\n"
154
+ for error in transition.errors:
155
+ result += f"\n - {error.message}"
156
+ return result
157
+
158
+
159
+ def _format_transition(source: str, status: str, transition: str, target: str) -> str:
160
+ return f"{source} -> [{status}] {transition} -> {target}"
161
+
162
+
163
+ class InvalidTransition(SchemathesisError):
164
+ """Raised when a stateful transition contains one or more errors."""
165
+
166
+ name: str
167
+ source: str
168
+ target: str
169
+ status_code: str
170
+ errors: list[TransitionValidationError]
171
+
172
+ __slots__ = ("name", "source", "target", "status_code", "errors")
173
+
174
+ def __init__(
175
+ self,
176
+ name: str,
177
+ source: str,
178
+ target: str,
179
+ status_code: str,
180
+ errors: list[TransitionValidationError],
181
+ ) -> None:
182
+ self.name = name
183
+ self.source = source
184
+ self.target = target
185
+ self.status_code = status_code
186
+ self.errors = errors
187
+
188
+
189
+ class TransitionValidationError(SchemathesisError):
190
+ """Single validation error found during stateful transition validation."""
191
+
192
+ message: str
193
+
194
+ __slots__ = ("message",)
195
+
196
+ def __init__(self, message: str) -> None:
197
+ self.message = message
198
+
199
+
200
+ class MalformedMediaType(ValueError):
201
+ """Raised on parsing of incorrect media type."""
202
+
203
+
204
+ class InvalidRegexPattern(InvalidSchema):
205
+ """Raised when a string pattern is not a valid regular expression."""
206
+
207
+ @classmethod
208
+ def from_hypothesis_jsonschema_message(cls, message: str) -> InvalidRegexPattern:
209
+ match = re.search(r"pattern='(.*?)'.*?\((.*?)\)", message)
210
+ if match:
211
+ message = f"Invalid regular expression. Pattern `{match.group(1)}` is not recognized - `{match.group(2)}`"
212
+ return cls(message)
213
+
214
+ @classmethod
215
+ def from_schema_error(cls, error: JsonSchemaError, *, from_examples: bool) -> InvalidRegexPattern:
216
+ if from_examples:
217
+ message = (
218
+ "Failed to generate test cases from examples for this API operation because of "
219
+ f"unsupported regular expression `{error.instance}`"
220
+ )
221
+ else:
222
+ message = (
223
+ "Failed to generate test cases for this API operation because of "
224
+ f"unsupported regular expression `{error.instance}`"
225
+ )
226
+ return cls(message)
227
+
228
+
229
+ class InvalidHeadersExample(InvalidSchema):
230
+ @classmethod
231
+ def from_headers(cls, headers: dict[str, str]) -> InvalidHeadersExample:
232
+ message = (
233
+ "Failed to generate test cases from examples for this API operation because of "
234
+ "some header examples are invalid:\n"
235
+ )
236
+ for key, value in headers.items():
237
+ message += f"\n - {key!r}={value!r}"
238
+ message += "\n\nEnsure the header examples comply with RFC 7230, Section 3.2"
239
+ return cls(message)
240
+
241
+
242
+ class IncorrectUsage(SchemathesisError):
243
+ """Indicates incorrect usage of Schemathesis' public API."""
244
+
245
+
246
+ class NoLinksFound(IncorrectUsage):
247
+ """Raised when no valid links are available for stateful testing."""
248
+
249
+
250
+ class InvalidRateLimit(IncorrectUsage):
251
+ """Incorrect input for rate limiting."""
252
+
253
+ def __init__(self, value: str) -> None:
254
+ super().__init__(
255
+ f"Invalid rate limit value: `{value}`. Should be in form `limit/interval`. "
256
+ "Example: `10/m` for 10 requests per minute."
257
+ )
258
+
259
+
260
+ class InternalError(SchemathesisError):
261
+ """Internal error in Schemathesis."""
262
+
263
+
264
+ class SerializationError(SchemathesisError):
265
+ """Can't serialize request payload."""
266
+
267
+
268
+ NAMESPACE_DEFINITION_URL = "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#xmlNamespace"
269
+ UNBOUND_PREFIX_MESSAGE_TEMPLATE = (
270
+ "Unbound prefix: `{prefix}`. "
271
+ "You need to define this namespace in your API schema via the `xml.namespace` keyword. "
272
+ f"See more at {NAMESPACE_DEFINITION_URL}"
273
+ )
274
+
275
+
276
+ class UnboundPrefix(SerializationError):
277
+ """XML serialization error.
278
+
279
+ It happens when the schema does not define a namespace that is used by some of its parts.
280
+ """
281
+
282
+ def __init__(self, prefix: str):
283
+ super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
284
+
285
+
286
+ class SerializationNotPossible(SerializationError):
287
+ """Not possible to serialize data to specified media type(s).
288
+
289
+ This error occurs in two scenarios:
290
+ 1. When attempting to serialize to a specific media type that isn't supported
291
+ 2. When none of the available media types can be used for serialization
292
+ """
293
+
294
+ def __init__(self, message: str, media_types: list[str]) -> None:
295
+ self.message = message
296
+ self.media_types = media_types
297
+
298
+ def __str__(self) -> str:
299
+ return self.message
300
+
301
+ @classmethod
302
+ def from_media_types(cls, *media_types: str) -> SerializationNotPossible:
303
+ """Create error when no available media type can be used."""
304
+ return cls(SERIALIZATION_NOT_POSSIBLE_MESSAGE.format(", ".join(media_types)), media_types=list(media_types))
305
+
306
+ @classmethod
307
+ def for_media_type(cls, media_type: str) -> SerializationNotPossible:
308
+ """Create error when a specific required media type isn't supported."""
309
+ return cls(SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE.format(media_type), media_types=[media_type])
310
+
311
+
312
+ class OperationNotFound(LookupError, SchemathesisError):
313
+ """Raised when an API operation cannot be found in the schema.
314
+
315
+ This error typically occurs during schema access in user code when trying to
316
+ reference a non-existent operation.
317
+ """
318
+
319
+ def __init__(self, message: str, item: str) -> None:
320
+ self.message = message
321
+ self.item = item
322
+
323
+ def __str__(self) -> str:
324
+ return self.message
325
+
326
+
327
+ @enum.unique
328
+ class LoaderErrorKind(str, enum.Enum):
329
+ # Connection related issues
330
+ CONNECTION_SSL = "connection_ssl"
331
+ CONNECTION_OTHER = "connection_other"
332
+ NETWORK_OTHER = "network_other"
333
+
334
+ # HTTP error codes
335
+ HTTP_SERVER_ERROR = "http_server_error"
336
+ HTTP_CLIENT_ERROR = "http_client_error"
337
+ HTTP_NOT_FOUND = "http_not_found"
338
+ HTTP_FORBIDDEN = "http_forbidden"
339
+
340
+ # Content decoding issues
341
+ SYNTAX_ERROR = "syntax_error"
342
+ UNEXPECTED_CONTENT_TYPE = "unexpected_content_type"
343
+ YAML_NUMERIC_STATUS_CODES = "yaml_numeric_status_codes"
344
+ YAML_NON_STRING_KEYS = "yaml_non_string_keys"
345
+
346
+ # Open API validation
347
+ OPEN_API_INVALID_SCHEMA = "open_api_invalid_schema"
348
+ OPEN_API_UNSPECIFIED_VERSION = "open_api_unspecified_version"
349
+ OPEN_API_UNSUPPORTED_VERSION = "open_api_unsupported_version"
350
+
351
+ # GraphQL validation
352
+ GRAPHQL_INVALID_SCHEMA = "graphql_invalid_schema"
353
+
354
+ # Unclassified
355
+ UNCLASSIFIED = "unclassified"
356
+
357
+
358
+ class LoaderError(SchemathesisError):
359
+ """Failed to load an API schema."""
360
+
361
+ def __init__(
362
+ self,
363
+ kind: LoaderErrorKind,
364
+ message: str,
365
+ url: str | None = None,
366
+ extras: list[str] | None = None,
367
+ ) -> None:
368
+ self.kind = kind
369
+ self.message = message
370
+ self.url = url
371
+ self.extras = extras or []
372
+
373
+ def __str__(self) -> str:
374
+ return self.message
375
+
376
+
377
+ def get_request_error_extras(exc: RequestException) -> list[str]:
378
+ """Extract additional context from a request exception."""
379
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
380
+ from urllib3.exceptions import MaxRetryError
381
+
382
+ if isinstance(exc, SSLError):
383
+ reason = str(exc.args[0].reason)
384
+ return [_remove_ssl_line_number(reason).strip()]
385
+ if isinstance(exc, ConnectionError):
386
+ inner = exc.args[0]
387
+ if isinstance(inner, MaxRetryError) and inner.reason is not None:
388
+ arg = inner.reason.args[0]
389
+ if isinstance(arg, str):
390
+ if ":" not in arg:
391
+ reason = arg
392
+ else:
393
+ _, reason = arg.split(":", maxsplit=1)
394
+ else:
395
+ reason = f"Max retries exceeded with url: {inner.url}"
396
+ return [reason.strip()]
397
+ return [" ".join(map(_clean_inner_request_message, inner.args))]
398
+ if isinstance(exc, ChunkedEncodingError):
399
+ args = exc.args[0].args
400
+ if len(args) == 1:
401
+ return [str(args[0])]
402
+ return [str(args[1])]
403
+ return []
404
+
405
+
406
+ def _remove_ssl_line_number(text: str) -> str:
407
+ return re.sub(r"\(_ssl\.c:\d+\)", "", text)
408
+
409
+
410
+ def _clean_inner_request_message(message: object) -> str:
411
+ if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
412
+ return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
413
+ return str(message)
414
+
415
+
416
+ def get_request_error_message(exc: RequestException) -> str:
417
+ """Extract user-facing message from a request exception."""
418
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, ReadTimeout, SSLError
419
+
420
+ if isinstance(exc, ReadTimeout):
421
+ _, duration = exc.args[0].args[0][:-1].split("read timeout=")
422
+ return f"Read timed out after {duration} seconds"
423
+ if isinstance(exc, SSLError):
424
+ return "SSL verification problem"
425
+ if isinstance(exc, ConnectionError):
426
+ return "Connection failed"
427
+ if isinstance(exc, ChunkedEncodingError):
428
+ return "Connection broken. The server declared chunked encoding but sent an invalid chunk"
429
+ return str(exc)
430
+
431
+
432
+ def split_traceback(traceback: str) -> list[str]:
433
+ return [entry for entry in traceback.splitlines() if entry]
434
+
435
+
436
+ def format_exception(
437
+ error: BaseException,
438
+ *,
439
+ with_traceback: bool = False,
440
+ skip_frames: int = 0,
441
+ ) -> str:
442
+ """Format exception with optional traceback."""
443
+ if not with_traceback:
444
+ lines = traceback.format_exception_only(type(error), error)
445
+ return "".join(lines).strip()
446
+
447
+ trace = error.__traceback__
448
+ if skip_frames > 0:
449
+ trace = extract_nth_traceback(trace, skip_frames)
450
+ lines = traceback.format_exception(type(error), error, trace)
451
+ return "".join(lines).strip()
452
+
453
+
454
+ def extract_nth_traceback(trace: TracebackType | None, n: int) -> TracebackType | None:
455
+ depth = 0
456
+ while depth < n and trace is not None:
457
+ trace = trace.tb_next
458
+ depth += 1
459
+ return trace