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.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {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
|
|
71
|
-
|
|
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
|
|
80
|
-
|
|
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 [
|
|
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 =
|
|
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
|
|
219
|
+
if len(elided) == 1 and contains_ref(elided[0]):
|
|
190
220
|
found.append(keyword)
|
|
191
221
|
return found
|
|
192
222
|
|