schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import lru_cache
5
+
6
+ try: # pragma: no cover
7
+ import re._constants as sre
8
+ import re._parser as sre_parse
9
+ except ImportError:
10
+ import sre_constants as sre
11
+ import sre_parse
12
+
13
+ ANCHOR = sre.AT
14
+ REPEATS: tuple
15
+ if hasattr(sre, "POSSESSIVE_REPEAT"):
16
+ REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT, sre.POSSESSIVE_REPEAT)
17
+ else:
18
+ REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
19
+ LITERAL = sre.LITERAL
20
+ IN = sre.IN
21
+ MAXREPEAT = sre_parse.MAXREPEAT
22
+
23
+
24
+ @lru_cache()
25
+ def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
26
+ """Update the quantifier of a regular expression based on given min and max lengths."""
27
+ if not pattern or (min_length in (None, 0) and max_length is None):
28
+ return pattern
29
+
30
+ try:
31
+ parsed = sre_parse.parse(pattern)
32
+ return _handle_parsed_pattern(parsed, pattern, min_length, max_length)
33
+ except re.error:
34
+ # Invalid pattern
35
+ return pattern
36
+
37
+
38
+ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
39
+ """Handle the parsed pattern and update quantifiers based on different cases."""
40
+ if len(parsed) == 1:
41
+ op, value = parsed[0]
42
+ return _update_quantifier(op, value, pattern, min_length, max_length)
43
+ elif len(parsed) == 2:
44
+ if parsed[0][0] == ANCHOR:
45
+ # Starts with an anchor
46
+ op, value = parsed[1]
47
+ anchor_length = _get_anchor_length(parsed[0][1])
48
+ leading_anchor = pattern[:anchor_length]
49
+ return leading_anchor + _update_quantifier(op, value, pattern[anchor_length:], min_length, max_length)
50
+ if parsed[1][0] == ANCHOR:
51
+ # Ends with an anchor
52
+ op, value = parsed[0]
53
+ anchor_length = _get_anchor_length(parsed[1][1])
54
+ trailing_anchor = pattern[-anchor_length:]
55
+ return _update_quantifier(op, value, pattern[:-anchor_length], min_length, max_length) + trailing_anchor
56
+ elif len(parsed) == 3 and parsed[0][0] == ANCHOR and parsed[2][0] == ANCHOR:
57
+ op, value = parsed[1]
58
+ leading_anchor_length = _get_anchor_length(parsed[0][1])
59
+ trailing_anchor_length = _get_anchor_length(parsed[2][1])
60
+ leading_anchor = pattern[:leading_anchor_length]
61
+ trailing_anchor = pattern[-trailing_anchor_length:]
62
+ return (
63
+ leading_anchor
64
+ + _update_quantifier(
65
+ op, value, pattern[leading_anchor_length:-trailing_anchor_length], min_length, max_length
66
+ )
67
+ + trailing_anchor
68
+ )
69
+ return pattern
70
+
71
+
72
+ def _get_anchor_length(node_type: int) -> int:
73
+ """Determine the length of the anchor based on its type."""
74
+ if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
75
+ return 2 # \A, \Z, \b, or \B
76
+ return 1 # ^ or $ or their multiline/locale/unicode variants
77
+
78
+
79
+ def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
80
+ """Update the quantifier based on the operation type and given constraints."""
81
+ if op in REPEATS:
82
+ return _handle_repeat_quantifier(value, pattern, min_length, max_length)
83
+ if op in (LITERAL, IN) and max_length != 0:
84
+ return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
85
+ return pattern
86
+
87
+
88
+ def _handle_repeat_quantifier(
89
+ value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
90
+ ) -> str:
91
+ """Handle repeat quantifiers (e.g., '+', '*', '?')."""
92
+ min_repeat, max_repeat, _ = value
93
+ min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
94
+ if min_length > max_length:
95
+ return pattern
96
+ return f"({_strip_quantifier(pattern)})" + _build_quantifier(min_length, max_length)
97
+
98
+
99
+ def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
100
+ """Handle literal or character class quantifiers."""
101
+ min_length = 1 if min_length is None else max(min_length, 1)
102
+ return f"({pattern})" + _build_quantifier(min_length, max_length)
103
+
104
+
105
+ def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
106
+ """Construct a quantifier string based on min and max values."""
107
+ if maximum == MAXREPEAT or maximum is None:
108
+ return f"{{{minimum or 0},}}"
109
+ if minimum == maximum:
110
+ return f"{{{minimum}}}"
111
+ return f"{{{minimum or 0},{maximum}}}"
112
+
113
+
114
+ def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_length: int | None) -> tuple[int, int]:
115
+ """Merge the current repetition constraints with the provided min and max lengths."""
116
+ if min_length is not None:
117
+ min_repeat = max(min_repeat, min_length)
118
+ if max_length is not None:
119
+ if max_repeat == MAXREPEAT:
120
+ max_repeat = max_length
121
+ else:
122
+ max_repeat = min(max_repeat, max_length)
123
+ return min_repeat, max_repeat
124
+
125
+
126
+ def _strip_quantifier(pattern: str) -> str:
127
+ """Remove quantifier from the pattern."""
128
+ # Lazy & posessive quantifiers
129
+ if pattern.endswith(("*?", "+?", "??", "*+", "?+", "++")):
130
+ return pattern[:-2]
131
+ if pattern.endswith(("?", "*", "+")):
132
+ pattern = pattern[:-1]
133
+ if pattern.endswith("}") and "{" in pattern:
134
+ # Find the start of the exact quantifier and drop everything since that index
135
+ idx = pattern.rfind("{")
136
+ pattern = pattern[:idx]
137
+ return pattern
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from dataclasses import dataclass
4
5
  from functools import lru_cache
5
6
  from typing import Any, Callable, Dict, Union, overload
@@ -7,6 +8,7 @@ from urllib.request import urlopen
7
8
 
8
9
  import jsonschema
9
10
  import requests
11
+ from jsonschema.exceptions import RefResolutionError
10
12
 
11
13
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
12
14
  from ...internal.copy import fast_deepcopy
@@ -55,6 +57,23 @@ class InliningResolver(jsonschema.RefResolver):
55
57
  )
56
58
  super().__init__(*args, **kwargs)
57
59
 
60
+ if sys.version_info >= (3, 11):
61
+
62
+ def resolve(self, ref: str) -> tuple[str, Any]:
63
+ try:
64
+ return super().resolve(ref)
65
+ except RefResolutionError as exc:
66
+ exc.add_note(ref)
67
+ raise
68
+ else:
69
+
70
+ def resolve(self, ref: str) -> tuple[str, Any]:
71
+ try:
72
+ return super().resolve(ref)
73
+ except RefResolutionError as exc:
74
+ exc.__notes__ = [ref]
75
+ raise
76
+
58
77
  @overload
59
78
  def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
60
79
  pass
@@ -65,10 +84,13 @@ class InliningResolver(jsonschema.RefResolver):
65
84
 
66
85
  def resolve_all(self, item: JSONType, recursion_level: int = 0) -> JSONType:
67
86
  """Recursively resolve all references in the given object."""
87
+ resolve = self.resolve_all
68
88
  if isinstance(item, dict):
69
89
  ref = item.get("$ref")
70
- if ref is not None and isinstance(ref, str):
71
- with self.resolving(ref) as resolved:
90
+ if isinstance(ref, str):
91
+ url, resolved = self.resolve(ref)
92
+ self.push_scope(url)
93
+ try:
72
94
  # If the next level of recursion exceeds the limit, then we need to copy it explicitly
73
95
  # In other cases, this method create new objects for mutable types (dict & list)
74
96
  next_recursion_level = recursion_level + 1
@@ -76,10 +98,18 @@ class InliningResolver(jsonschema.RefResolver):
76
98
  copied = fast_deepcopy(resolved)
77
99
  remove_optional_references(copied)
78
100
  return copied
79
- return self.resolve_all(resolved, next_recursion_level)
80
- return {key: self.resolve_all(sub_item, recursion_level) for key, sub_item in item.items()}
101
+ return resolve(resolved, next_recursion_level)
102
+ finally:
103
+ self.pop_scope()
104
+ return {
105
+ key: resolve(sub_item, recursion_level) if isinstance(sub_item, (dict, list)) else sub_item
106
+ for key, sub_item in item.items()
107
+ }
81
108
  if isinstance(item, list):
82
- return [self.resolve_all(sub_item, recursion_level) for sub_item in item]
109
+ return [
110
+ self.resolve_all(sub_item, recursion_level) if isinstance(sub_item, (dict, list)) else sub_item
111
+ for sub_item in item
112
+ ]
83
113
  return item
84
114
 
85
115
  def resolve_in_scope(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any]]:
@@ -89,7 +119,7 @@ class InliningResolver(jsonschema.RefResolver):
89
119
  if "$ref" in definition:
90
120
  self.push_scope(scope)
91
121
  try:
92
- new_scope, definition = fast_deepcopy(self.resolve(definition["$ref"]))
122
+ new_scope, definition = self.resolve(definition["$ref"])
93
123
  finally:
94
124
  self.pop_scope()
95
125
  scopes.append(new_scope)
@@ -186,7 +216,7 @@ def remove_optional_references(schema: dict[str, Any]) -> None:
186
216
  v = s.get(keyword)
187
217
  if v is not None:
188
218
  elided = [sub for sub in v if not can_elide(sub)]
189
- if len(elided) == 1 and "$ref" in elided[0]:
219
+ if len(elided) == 1 and contains_ref(elided[0]):
190
220
  found.append(keyword)
191
221
  return found
192
222