schemathesis 4.0.0a2__py3-none-any.whl → 4.0.0a3__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/cli/__init__.py +12 -1
- schemathesis/cli/commands/run/events.py +22 -2
- schemathesis/cli/commands/run/executor.py +3 -0
- schemathesis/cli/commands/run/handlers/output.py +108 -71
- schemathesis/core/errors.py +8 -0
- schemathesis/engine/core.py +1 -1
- schemathesis/engine/errors.py +11 -5
- schemathesis/engine/phases/stateful/__init__.py +1 -0
- schemathesis/engine/phases/stateful/_executor.py +8 -11
- schemathesis/engine/recorder.py +22 -21
- schemathesis/errors.py +19 -13
- schemathesis/generation/coverage.py +4 -4
- schemathesis/generation/hypothesis/builder.py +15 -12
- schemathesis/generation/stateful/state_machine.py +8 -9
- schemathesis/specs/openapi/checks.py +50 -27
- schemathesis/specs/openapi/links.py +18 -4
- schemathesis/specs/openapi/patterns.py +170 -2
- schemathesis/specs/openapi/schemas.py +54 -26
- schemathesis/specs/openapi/stateful/__init__.py +124 -74
- schemathesis/specs/openapi/stateful/control.py +87 -0
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a3.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a3.dist-info}/RECORD +25 -24
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a3.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a3.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a2.dist-info → schemathesis-4.0.0a3.dist-info}/licenses/LICENSE +0 -0
@@ -66,8 +66,8 @@ from .stateful import create_state_machine
|
|
66
66
|
if TYPE_CHECKING:
|
67
67
|
from hypothesis.strategies import SearchStrategy
|
68
68
|
|
69
|
-
from
|
70
|
-
from
|
69
|
+
from schemathesis.auths import AuthStorage
|
70
|
+
from schemathesis.generation.stateful import APIStateMachine
|
71
71
|
|
72
72
|
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
73
73
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
@@ -174,30 +174,64 @@ class BaseOpenAPISchema(BaseSchema):
|
|
174
174
|
return statistic
|
175
175
|
|
176
176
|
resolve = self.resolver.resolve
|
177
|
+
resolve_path_item = self._resolve_path_item
|
177
178
|
should_skip = self._should_skip
|
178
179
|
links_field = self.links_field
|
179
180
|
|
181
|
+
# For operationId lookup
|
182
|
+
selected_operations_by_id: set[str] = set()
|
183
|
+
# Tuples of (method, path)
|
184
|
+
selected_operations_by_path: set[tuple[str, str]] = set()
|
185
|
+
collected_links: list[dict] = []
|
186
|
+
|
180
187
|
for path, path_item in paths.items():
|
181
188
|
try:
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
if
|
198
|
-
|
189
|
+
scope, path_item = resolve_path_item(path_item)
|
190
|
+
self.resolver.push_scope(scope)
|
191
|
+
try:
|
192
|
+
for method, definition in path_item.items():
|
193
|
+
if method not in HTTP_METHODS:
|
194
|
+
continue
|
195
|
+
statistic.operations.total += 1
|
196
|
+
is_selected = not should_skip(path, method, definition)
|
197
|
+
if is_selected:
|
198
|
+
statistic.operations.selected += 1
|
199
|
+
# Store both identifiers
|
200
|
+
if "operationId" in definition:
|
201
|
+
selected_operations_by_id.add(definition["operationId"])
|
202
|
+
selected_operations_by_path.add((method, path))
|
203
|
+
for response in definition.get("responses", {}).values():
|
204
|
+
if "$ref" in response:
|
205
|
+
_, response = resolve(response["$ref"])
|
206
|
+
defined_links = response.get(links_field)
|
207
|
+
if defined_links is not None:
|
208
|
+
statistic.links.total += len(defined_links)
|
209
|
+
if is_selected:
|
210
|
+
collected_links.extend(defined_links.values())
|
211
|
+
finally:
|
212
|
+
self.resolver.pop_scope()
|
199
213
|
except SCHEMA_PARSING_ERRORS:
|
200
214
|
continue
|
215
|
+
|
216
|
+
def is_link_selected(link: dict) -> bool:
|
217
|
+
if "$ref" in link:
|
218
|
+
_, link = resolve(link["$ref"])
|
219
|
+
|
220
|
+
if "operationId" in link:
|
221
|
+
return link["operationId"] in selected_operations_by_id
|
222
|
+
else:
|
223
|
+
try:
|
224
|
+
scope, _ = resolve(link["operationRef"])
|
225
|
+
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
226
|
+
path = path.replace("~1", "/").replace("~0", "~")
|
227
|
+
return (method, path) in selected_operations_by_path
|
228
|
+
except Exception:
|
229
|
+
return False
|
230
|
+
|
231
|
+
for link in collected_links:
|
232
|
+
if is_link_selected(link):
|
233
|
+
statistic.links.selected += 1
|
234
|
+
|
201
235
|
return statistic
|
202
236
|
|
203
237
|
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
@@ -569,13 +603,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
569
603
|
return scopes, definitions.get("headers")
|
570
604
|
|
571
605
|
def as_state_machine(self) -> type[APIStateMachine]:
|
572
|
-
|
573
|
-
return create_state_machine(self)
|
574
|
-
except OperationNotFound as exc:
|
575
|
-
raise LoaderError(
|
576
|
-
kind=LoaderErrorKind.OPEN_API_INVALID_SCHEMA,
|
577
|
-
message=f"Invalid Open API link definition: Operation `{exc.item}` not found",
|
578
|
-
) from exc
|
606
|
+
return create_state_machine(self)
|
579
607
|
|
580
608
|
def add_link(
|
581
609
|
self,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
3
|
+
from dataclasses import dataclass
|
4
4
|
from functools import lru_cache
|
5
5
|
from typing import TYPE_CHECKING, Any, Callable, Iterator
|
6
6
|
|
@@ -8,25 +8,31 @@ from hypothesis import strategies as st
|
|
8
8
|
from hypothesis.stateful import Bundle, Rule, precondition, rule
|
9
9
|
|
10
10
|
from schemathesis.core.result import Ok
|
11
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
12
|
+
from schemathesis.generation import GenerationMode
|
11
13
|
from schemathesis.generation.case import Case
|
12
14
|
from schemathesis.generation.hypothesis import strategies
|
13
15
|
from schemathesis.generation.stateful.state_machine import APIStateMachine, StepInput, StepOutput, _normalize_name
|
14
16
|
from schemathesis.schemas import APIOperation
|
15
|
-
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from ..utils import expand_status_code
|
17
|
+
from schemathesis.specs.openapi.links import OpenApiLink, get_all_links
|
18
|
+
from schemathesis.specs.openapi.stateful.control import TransitionController
|
19
|
+
from schemathesis.specs.openapi.utils import expand_status_code
|
19
20
|
|
20
21
|
if TYPE_CHECKING:
|
21
22
|
from schemathesis.generation.stateful.state_machine import StepOutput
|
22
|
-
|
23
|
-
from ..schemas import BaseOpenAPISchema
|
23
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
24
24
|
|
25
25
|
FilterFunction = Callable[["StepOutput"], bool]
|
26
26
|
|
27
27
|
|
28
28
|
class OpenAPIStateMachine(APIStateMachine):
|
29
29
|
_response_matchers: dict[str, Callable[[StepOutput], str | None]]
|
30
|
+
_transitions: ApiTransitions
|
31
|
+
|
32
|
+
def __init__(self) -> None:
|
33
|
+
self.recorder = ScenarioRecorder(label="Stateful tests")
|
34
|
+
self.control = TransitionController(self._transitions)
|
35
|
+
super().__init__()
|
30
36
|
|
31
37
|
def _get_target_for_result(self, result: StepOutput) -> str | None:
|
32
38
|
matcher = self._response_matchers.get(result.case.operation.label)
|
@@ -36,81 +42,118 @@ class OpenAPIStateMachine(APIStateMachine):
|
|
36
42
|
|
37
43
|
|
38
44
|
# The proportion of negative tests generated for "root" transitions
|
39
|
-
NEGATIVE_TEST_CASES_THRESHOLD =
|
45
|
+
NEGATIVE_TEST_CASES_THRESHOLD = 10
|
40
46
|
|
41
47
|
|
42
|
-
|
43
|
-
|
48
|
+
@dataclass
|
49
|
+
class OperationTransitions:
|
50
|
+
"""Transitions for a single operation."""
|
44
51
|
|
45
|
-
|
46
|
-
1. POST /users/
|
47
|
-
2. GET /users/{id}/
|
52
|
+
__slots__ = ("incoming", "outgoing")
|
48
53
|
|
49
|
-
|
50
|
-
|
54
|
+
def __init__(self) -> None:
|
55
|
+
self.incoming: list[OpenApiLink] = []
|
56
|
+
self.outgoing: list[OpenApiLink] = []
|
57
|
+
|
58
|
+
|
59
|
+
@dataclass
|
60
|
+
class ApiTransitions:
|
61
|
+
"""Stores all transitions grouped by operation."""
|
62
|
+
|
63
|
+
__slots__ = ("operations",)
|
64
|
+
|
65
|
+
def __init__(self) -> None:
|
66
|
+
# operation label -> its transitions
|
67
|
+
self.operations: dict[str, OperationTransitions] = {}
|
68
|
+
|
69
|
+
def add_outgoing(self, source: str, link: OpenApiLink) -> None:
|
70
|
+
"""Record an outgoing transition from source operation."""
|
71
|
+
self.operations.setdefault(source, OperationTransitions()).outgoing.append(link)
|
72
|
+
self.operations.setdefault(link.target.label, OperationTransitions()).incoming.append(link)
|
73
|
+
|
74
|
+
|
75
|
+
def collect_transitions(operations: list[APIOperation]) -> ApiTransitions:
|
76
|
+
"""Collect all transitions between operations."""
|
77
|
+
transitions = ApiTransitions()
|
78
|
+
|
79
|
+
selected_labels = {operation.label for operation in operations}
|
80
|
+
for operation in operations:
|
81
|
+
for _, link in get_all_links(operation):
|
82
|
+
if link.target.label in selected_labels:
|
83
|
+
transitions.add_outgoing(operation.label, link)
|
84
|
+
|
85
|
+
return transitions
|
86
|
+
|
87
|
+
|
88
|
+
def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
51
89
|
operations = [result.ok() for result in schema.get_all_operations() if isinstance(result, Ok)]
|
52
90
|
bundles = {}
|
53
|
-
|
91
|
+
transitions = collect_transitions(operations)
|
54
92
|
_response_matchers: dict[str, Callable[[StepOutput], str | None]] = {}
|
55
|
-
|
93
|
+
|
94
|
+
# Create bundles and matchers
|
56
95
|
for operation in operations:
|
57
96
|
all_status_codes = tuple(operation.definition.raw["responses"])
|
58
97
|
bundle_matchers = []
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
98
|
+
|
99
|
+
if operation.label in transitions.operations:
|
100
|
+
# Use outgoing transitions
|
101
|
+
for link in transitions.operations[operation.label].outgoing:
|
102
|
+
bundle_name = f"{operation.label} -> {link.status_code}"
|
103
|
+
bundles[bundle_name] = Bundle(bundle_name)
|
104
|
+
bundle_matchers.append((bundle_name, make_response_filter(link.status_code, all_status_codes)))
|
105
|
+
|
64
106
|
if bundle_matchers:
|
65
107
|
_response_matchers[operation.label] = make_response_matcher(bundle_matchers)
|
108
|
+
|
66
109
|
rules = {}
|
67
110
|
catch_all = Bundle("catch_all")
|
68
111
|
|
69
112
|
for target in operations:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
113
|
+
if target.label in transitions.operations:
|
114
|
+
incoming = transitions.operations[target.label].incoming
|
115
|
+
if incoming:
|
116
|
+
for link in incoming:
|
117
|
+
bundle_name = f"{link.source.label} -> {link.status_code}"
|
118
|
+
name = _normalize_name(f"{link.status_code} -> {target.label}")
|
119
|
+
name = _normalize_name(f"{link.source.label} -> {link.status_code} -> {target.label}")
|
120
|
+
assert name not in rules
|
121
|
+
rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
|
122
|
+
transition(
|
123
|
+
name=name,
|
124
|
+
target=catch_all,
|
125
|
+
input=bundles[bundle_name].flatmap(
|
126
|
+
into_step_input(target=target, link=link, modes=schema.generation_config.modes)
|
127
|
+
),
|
128
|
+
)
|
82
129
|
)
|
130
|
+
if transitions.operations[target.label].outgoing and target.method == "post":
|
131
|
+
# Allow POST methods for operations with outgoing transitions.
|
132
|
+
# This approach also includes cases when there is an incoming transition back to POST
|
133
|
+
# For example, POST /users/ -> GET /users/{id}/
|
134
|
+
# The source operation has no prerequisite, but we need to allow this rule to be executed
|
135
|
+
# in order to reach other transitions
|
136
|
+
name = _normalize_name(f"{target.label} -> X")
|
137
|
+
if len(schema.generation_config.modes) == 1:
|
138
|
+
case_strategy = target.as_strategy(generation_mode=schema.generation_config.modes[0])
|
139
|
+
else:
|
140
|
+
_strategies = {
|
141
|
+
method: target.as_strategy(generation_mode=method) for method in schema.generation_config.modes
|
142
|
+
}
|
143
|
+
|
144
|
+
@st.composite # type: ignore[misc]
|
145
|
+
def case_strategy_factory(
|
146
|
+
draw: st.DrawFn, strategies: dict[GenerationMode, st.SearchStrategy] = _strategies
|
147
|
+
) -> Case:
|
148
|
+
if draw(st.integers(min_value=0, max_value=99)) < NEGATIVE_TEST_CASES_THRESHOLD:
|
149
|
+
return draw(strategies[GenerationMode.NEGATIVE])
|
150
|
+
return draw(strategies[GenerationMode.POSITIVE])
|
151
|
+
|
152
|
+
case_strategy = case_strategy_factory()
|
153
|
+
|
154
|
+
rules[name] = precondition(is_root_allowed(target.label))(
|
155
|
+
transition(name=name, target=catch_all, input=case_strategy.map(StepInput.initial))
|
83
156
|
)
|
84
|
-
elif any(
|
85
|
-
incoming.source.label == target.label
|
86
|
-
for transitions in incoming_transitions.values()
|
87
|
-
for incoming in transitions
|
88
|
-
):
|
89
|
-
# No incoming transitions, but has at least one outgoing transition
|
90
|
-
# For example, POST /users/ -> GET /users/{id}/
|
91
|
-
# The source operation has no prerequisite, but we need to allow this rule to be executed
|
92
|
-
# in order to reach other transitions
|
93
|
-
name = _normalize_name(f"{target.label} -> X")
|
94
|
-
if len(schema.generation_config.modes) == 1:
|
95
|
-
case_strategy = target.as_strategy(generation_mode=schema.generation_config.modes[0])
|
96
|
-
else:
|
97
|
-
_strategies = {
|
98
|
-
method: target.as_strategy(generation_mode=method) for method in schema.generation_config.modes
|
99
|
-
}
|
100
|
-
|
101
|
-
@st.composite # type: ignore[misc]
|
102
|
-
def case_strategy_factory(
|
103
|
-
draw: st.DrawFn, strategies: dict[GenerationMode, st.SearchStrategy] = _strategies
|
104
|
-
) -> Case:
|
105
|
-
if draw(st.integers(min_value=0, max_value=99)) < NEGATIVE_TEST_CASES_THRESHOLD:
|
106
|
-
return draw(strategies[GenerationMode.NEGATIVE])
|
107
|
-
return draw(strategies[GenerationMode.POSITIVE])
|
108
|
-
|
109
|
-
case_strategy = case_strategy_factory()
|
110
|
-
|
111
|
-
rules[name] = precondition(ensure_links_followed)(
|
112
|
-
transition(name=name, target=catch_all, input=case_strategy.map(StepInput.initial))
|
113
|
-
)
|
114
157
|
|
115
158
|
return type(
|
116
159
|
"APIWorkflow",
|
@@ -119,6 +162,7 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
119
162
|
"schema": schema,
|
120
163
|
"bundles": bundles,
|
121
164
|
"_response_matchers": _response_matchers,
|
165
|
+
"_transitions": transitions,
|
122
166
|
**rules,
|
123
167
|
},
|
124
168
|
)
|
@@ -165,23 +209,29 @@ def into_step_input(
|
|
165
209
|
return builder
|
166
210
|
|
167
211
|
|
168
|
-
def
|
169
|
-
def inner(machine:
|
170
|
-
return bool(machine.bundles.get(bundle_name))
|
212
|
+
def is_transition_allowed(bundle_name: str, source: str, target: str) -> Callable[[OpenAPIStateMachine], bool]:
|
213
|
+
def inner(machine: OpenAPIStateMachine) -> bool:
|
214
|
+
return bool(machine.bundles.get(bundle_name)) and machine.control.allow_transition(source, target)
|
171
215
|
|
172
216
|
return inner
|
173
217
|
|
174
218
|
|
175
|
-
def
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
return True
|
219
|
+
def is_root_allowed(label: str) -> Callable[[OpenAPIStateMachine], bool]:
|
220
|
+
def inner(machine: OpenAPIStateMachine) -> bool:
|
221
|
+
return machine.control.allow_root_transition(label, machine.bundles)
|
222
|
+
|
223
|
+
return inner
|
181
224
|
|
182
225
|
|
183
226
|
def transition(*, name: str, target: Bundle, input: st.SearchStrategy[StepInput]) -> Callable[[Callable], Rule]:
|
184
|
-
def step_function(self:
|
227
|
+
def step_function(self: OpenAPIStateMachine, input: StepInput) -> StepOutput | None:
|
228
|
+
if input.transition is not None:
|
229
|
+
self.recorder.record_case(
|
230
|
+
parent_id=input.transition.parent_id, transition=input.transition, case=input.case
|
231
|
+
)
|
232
|
+
else:
|
233
|
+
self.recorder.record_case(parent_id=None, transition=None, case=input.case)
|
234
|
+
self.control.record_step(input, self.recorder)
|
185
235
|
return APIStateMachine._step(self, input=input)
|
186
236
|
|
187
237
|
step_function.__name__ = name
|
@@ -0,0 +1,87 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import Counter
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
8
|
+
from schemathesis.generation.stateful.state_machine import DEFAULT_STATEFUL_STEP_COUNT
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from requests.structures import CaseInsensitiveDict
|
12
|
+
|
13
|
+
from schemathesis.generation.stateful.state_machine import StepInput
|
14
|
+
from schemathesis.specs.openapi.stateful import ApiTransitions
|
15
|
+
|
16
|
+
|
17
|
+
# It is enough to be able to catch double-click type of issues
|
18
|
+
MAX_OPERATIONS_PER_SOURCE_CAP = 2
|
19
|
+
# Maximum number of concurrent root sources (e.g., active users in the system)
|
20
|
+
MAX_ROOT_SOURCES = 2
|
21
|
+
|
22
|
+
|
23
|
+
def _get_max_operations_per_source(transitions: ApiTransitions) -> int:
|
24
|
+
"""Calculate global limit based on number of sources to maximize diversity of used API calls."""
|
25
|
+
sources = len(transitions.operations)
|
26
|
+
|
27
|
+
if sources == 0:
|
28
|
+
return MAX_OPERATIONS_PER_SOURCE_CAP
|
29
|
+
|
30
|
+
# Total steps divided by number of sources, but never below the cap
|
31
|
+
return max(MAX_OPERATIONS_PER_SOURCE_CAP, DEFAULT_STATEFUL_STEP_COUNT // sources)
|
32
|
+
|
33
|
+
|
34
|
+
@dataclass
|
35
|
+
class TransitionController:
|
36
|
+
"""Controls which transitions can be executed in a state machine."""
|
37
|
+
|
38
|
+
__slots__ = ("transitions", "max_operations_per_source", "statistic")
|
39
|
+
|
40
|
+
def __init__(self, transitions: ApiTransitions) -> None:
|
41
|
+
# Incoming & outgoing transitions available in the state machine
|
42
|
+
self.transitions = transitions
|
43
|
+
self.max_operations_per_source = _get_max_operations_per_source(transitions)
|
44
|
+
# source -> derived API calls
|
45
|
+
self.statistic: dict[str, dict[str, Counter[str]]] = {}
|
46
|
+
|
47
|
+
def record_step(self, input: StepInput, recorder: ScenarioRecorder) -> None:
|
48
|
+
"""Record API call input."""
|
49
|
+
case = input.case
|
50
|
+
|
51
|
+
if (
|
52
|
+
case.operation.label in self.transitions.operations
|
53
|
+
and self.transitions.operations[case.operation.label].outgoing
|
54
|
+
):
|
55
|
+
# This API operation has outgoing transitions, hence record it as a source
|
56
|
+
entry = self.statistic.setdefault(input.case.operation.label, {})
|
57
|
+
entry[input.case.id] = Counter()
|
58
|
+
|
59
|
+
if input.transition is not None:
|
60
|
+
# Find immediate parent and record as derived operation
|
61
|
+
parent = recorder.cases[input.transition.parent_id]
|
62
|
+
source = parent.value.operation.label
|
63
|
+
case_id = parent.value.id
|
64
|
+
|
65
|
+
if source in self.statistic and case_id in self.statistic[source]:
|
66
|
+
self.statistic[source][case_id][case.operation.label] += 1
|
67
|
+
|
68
|
+
def allow_root_transition(self, source: str, bundles: dict[str, CaseInsensitiveDict]) -> bool:
|
69
|
+
"""Decide if this root transition should be allowed now."""
|
70
|
+
if len(self.statistic.get(source, {})) < MAX_ROOT_SOURCES:
|
71
|
+
return True
|
72
|
+
|
73
|
+
# If all non-root operations are blocked, then allow root ones to make progress
|
74
|
+
history = {name.split("->")[0].strip() for name, values in bundles.items() if values}
|
75
|
+
return all(
|
76
|
+
incoming.source.label not in history
|
77
|
+
or not self.allow_transition(incoming.source.label, incoming.target.label)
|
78
|
+
for transitions in self.transitions.operations.values()
|
79
|
+
for incoming in transitions.incoming
|
80
|
+
if transitions.incoming
|
81
|
+
)
|
82
|
+
|
83
|
+
def allow_transition(self, source: str, target: str) -> bool:
|
84
|
+
"""Decide if this transition should be allowed now."""
|
85
|
+
existing = self.statistic.get(source, {})
|
86
|
+
total = sum(metric.get(target, 0) for metric in existing.values())
|
87
|
+
return total < self.max_operations_per_source
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 4.0.
|
3
|
+
Version: 4.0.0a3
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
@@ -1,12 +1,12 @@
|
|
1
1
|
schemathesis/__init__.py,sha256=ggp1CxctLo__wwFwlDhvtrexxDXGSbRjFKzXw_Twi7k,1139
|
2
2
|
schemathesis/auths.py,sha256=t-YuPyoLqL7jlRUH-45JxO7Ir3pYxpe31CRmNIJh7rI,15423
|
3
3
|
schemathesis/checks.py,sha256=B5-ROnjvvwpaqgj_iQ7eCjGqvRRVT30eWNPLKmwdrM8,5084
|
4
|
-
schemathesis/errors.py,sha256=
|
4
|
+
schemathesis/errors.py,sha256=R_GkKg9o-XYWsU4f-Devn_clIrhMi5ZXpiAjqi5Tupw,791
|
5
5
|
schemathesis/filters.py,sha256=6kffe_Xbi7-xThsUciWfmy1IU-LBgSYkXROUJJONJ48,13311
|
6
6
|
schemathesis/hooks.py,sha256=jTdN5GJbxHRMshxgcuI_th9ouuL32CN4m2Jt0pmT_bs,13148
|
7
7
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
schemathesis/schemas.py,sha256=wQqKfZIaPgfAzLVUOxkgW7LqwBdWtLJeryeouytUlTw,27412
|
9
|
-
schemathesis/cli/__init__.py,sha256=
|
9
|
+
schemathesis/cli/__init__.py,sha256=cT8DmZYudre1a0cX2KOBytquJ85CMdmMnqqMZsIATJU,822
|
10
10
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
11
11
|
schemathesis/cli/constants.py,sha256=rUixnqorraUFDtOu3Nmm1x_k0qbgmW9xW96kQB_fBCQ,338
|
12
12
|
schemathesis/cli/core.py,sha256=Qm5xvpIIMwJDTeR3N3TjKhMCHV5d5Rp0UstVS2GjWgw,459
|
@@ -15,8 +15,8 @@ schemathesis/cli/commands/__init__.py,sha256=FFalEss3D7mnCRO0udtYb65onXSjQCCOv8s
|
|
15
15
|
schemathesis/cli/commands/run/__init__.py,sha256=mpXoFTAuyaBjTQiusLVfzU9BXwcNN84vcmll22bQ-18,22651
|
16
16
|
schemathesis/cli/commands/run/checks.py,sha256=-p6bzuc98kNR1VhkTI2s4xaJx-b5zYSMwdlhONwUhRM,3337
|
17
17
|
schemathesis/cli/commands/run/context.py,sha256=o_lUkR2bpsxO4oMB7eJVCMIR9jmfS0nLZCd_LdOluF4,3906
|
18
|
-
schemathesis/cli/commands/run/events.py,sha256=
|
19
|
-
schemathesis/cli/commands/run/executor.py,sha256=
|
18
|
+
schemathesis/cli/commands/run/events.py,sha256=Dj-xvIr-Hkms8kvh4whNwKSk1Q2Hx4NIENi_4A8nQO8,1224
|
19
|
+
schemathesis/cli/commands/run/executor.py,sha256=pozPf5kzizsQtCzssE_5M8AV2lgzPcE3o9xxoOA1n78,4816
|
20
20
|
schemathesis/cli/commands/run/filters.py,sha256=vBYH_Q1Gc2yjo4pysd1nSKfNVQADh5POFoAExvUPmqQ,7390
|
21
21
|
schemathesis/cli/commands/run/hypothesis.py,sha256=3PkcUSe-P6zgkhPJbN8RstjGNzgcY76AVlJ7ayu44Eg,3582
|
22
22
|
schemathesis/cli/commands/run/loaders.py,sha256=VedoeIE1tgFBqVokWxOoUReAjBl-Zhx87RjCEBtCVfs,4840
|
@@ -25,7 +25,7 @@ schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31M
|
|
25
25
|
schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
|
26
26
|
schemathesis/cli/commands/run/handlers/cassettes.py,sha256=y4gtGnAfhE9dskhXPnDAQSx3kY4R2a1hvAiE3o8AsUA,19001
|
27
27
|
schemathesis/cli/commands/run/handlers/junitxml.py,sha256=MkFq_DyMWves4Ytj72-4secMTlBOt7Gs9W3nfBzknIA,2316
|
28
|
-
schemathesis/cli/commands/run/handlers/output.py,sha256=
|
28
|
+
schemathesis/cli/commands/run/handlers/output.py,sha256=KlT_omUWRWzygvqz_nwRvXJXjtbmzTYhfWqPCLOQTBw,51917
|
29
29
|
schemathesis/cli/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
30
|
schemathesis/cli/ext/fs.py,sha256=OA3mRzra4rq3NyDTcBvlRh0WJrh4ByN-QQ8loI04m88,408
|
31
31
|
schemathesis/cli/ext/groups.py,sha256=9mIZUnQasUxGz6gZA0IoGVGKtSQuVNl8w2pwX_a1nBk,1796
|
@@ -38,7 +38,7 @@ schemathesis/core/compat.py,sha256=Lflo6z-nQ6S4uKZINc4Fr90pd3LTN6cIG9HJJmmaHeY,7
|
|
38
38
|
schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,130
|
39
39
|
schemathesis/core/curl.py,sha256=yuaCe_zHLGwUjEeloQi6W3tOA3cGdnHDNI17-5jia0o,1723
|
40
40
|
schemathesis/core/deserialization.py,sha256=ygIj4fNaOd0mJ2IvTsn6bsabBt_2AbSLCz-z9UqfpdQ,2406
|
41
|
-
schemathesis/core/errors.py,sha256=
|
41
|
+
schemathesis/core/errors.py,sha256=ocwWs8xtoiTtzGn8eCdCtb_Hrdn-jHi_d_TNl29UIkI,13656
|
42
42
|
schemathesis/core/failures.py,sha256=4ftUsY0q1QqqfWpYkl5lWCZVBUAMy8VP9pXsyDZ8Q3o,8723
|
43
43
|
schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
|
44
44
|
schemathesis/core/lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
@@ -58,14 +58,14 @@ schemathesis/engine/__init__.py,sha256=xncZMXY8S-v4mrfnW4CK6-RQ0S0bigfLDJScpQysb
|
|
58
58
|
schemathesis/engine/config.py,sha256=vWwtaWuSLvE-w0S9n4_MlJADjN58zlgSp84ZQS2z0ls,1919
|
59
59
|
schemathesis/engine/context.py,sha256=HeLX-0aqSAbXJe_ZlkqVfg3QlhmbCrazbb9-ZPbi0h0,3723
|
60
60
|
schemathesis/engine/control.py,sha256=QKUOs5VMphe7EcAIro_DDo9ZqdOU6ZVwTU1gMNndHWw,1006
|
61
|
-
schemathesis/engine/core.py,sha256=
|
62
|
-
schemathesis/engine/errors.py,sha256=
|
61
|
+
schemathesis/engine/core.py,sha256=_Z89tc_aWQ0kHajVXmoTaEbIRYeERvb1s0rD82XpYxg,5085
|
62
|
+
schemathesis/engine/errors.py,sha256=EtgqS-6ngtFg6TtloN_g07t3wYPMF6JOSesZ3C2VyXY,16576
|
63
63
|
schemathesis/engine/events.py,sha256=7tpxCxptKx_WvcFgv9_6v0E-5wiQfDs1O5bsOH46xb8,5989
|
64
|
-
schemathesis/engine/recorder.py,sha256=
|
64
|
+
schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
|
65
65
|
schemathesis/engine/phases/__init__.py,sha256=HZmlOjGvtDkfTwAw2rJFcfsJ2qg2h973l4zDy3AzsQg,2034
|
66
66
|
schemathesis/engine/phases/probes.py,sha256=3M9g3E7CXbDDK_8inuvkRZibCCcoO2Ce5U3lnyTeWXQ,5131
|
67
|
-
schemathesis/engine/phases/stateful/__init__.py,sha256=
|
68
|
-
schemathesis/engine/phases/stateful/_executor.py,sha256=
|
67
|
+
schemathesis/engine/phases/stateful/__init__.py,sha256=lWo2RLrutNblHvohTzofQqL22GORwBRA8bf6jvLuGPg,2391
|
68
|
+
schemathesis/engine/phases/stateful/_executor.py,sha256=oZSfgvGUmiyjVDGpTeZCRv8KLjesYXJeE3cs0OTkQ1E,12428
|
69
69
|
schemathesis/engine/phases/stateful/context.py,sha256=SKWsok-tlWbUDagiUmP7cLNW6DsgFDc_Afv0vQfWv6c,2964
|
70
70
|
schemathesis/engine/phases/unit/__init__.py,sha256=XgLGwkVemmobLP6cyfAGYLu8RFRCrv6osfB3BD6OAS0,7537
|
71
71
|
schemathesis/engine/phases/unit/_executor.py,sha256=X8hziN6rAd9vRTkbiMKcZWN6ujj4pUEiVdm6OmFsniI,12931
|
@@ -73,19 +73,19 @@ schemathesis/engine/phases/unit/_pool.py,sha256=01xRGJnmfLqGBH-f3nQEDv7vOufmen5Z
|
|
73
73
|
schemathesis/experimental/__init__.py,sha256=36H1vLQhrw4SMD_jx76Wt07PHneELRDY1jfBSh7VxU0,2257
|
74
74
|
schemathesis/generation/__init__.py,sha256=2htA0TlQee6AvQmLl1VNxEptRDqvPjksXKJLMVLAJng,1580
|
75
75
|
schemathesis/generation/case.py,sha256=Rt5MCUtPVYVQzNyjUx8magocPJpHV1svyuqQSTwUE-I,7306
|
76
|
-
schemathesis/generation/coverage.py,sha256=
|
76
|
+
schemathesis/generation/coverage.py,sha256=yQwazaxhKacHU3sA8shMPWQ1JhLE_90YgIZjvAymsG8,39133
|
77
77
|
schemathesis/generation/meta.py,sha256=36h6m4E7jzLGa8TCvl7eBl_xUWLiRul3qxzexl5cB58,2515
|
78
78
|
schemathesis/generation/modes.py,sha256=t_EvKr2aOXYMsEfdMu4lLF4KCGcX1LVVyvzTkcpJqhk,663
|
79
79
|
schemathesis/generation/overrides.py,sha256=FhqcFoliEvgW6MZyFPYemfLgzKt3Miy8Cud7OMOCb7g,3045
|
80
80
|
schemathesis/generation/targets.py,sha256=_rN2qgxTE2EfvygiN-Fy3WmDnRH0ERohdx3sKRDaYhU,2120
|
81
81
|
schemathesis/generation/hypothesis/__init__.py,sha256=Rl7QwvMBMJI7pBqTydplX6bXC420n0EGQHVm-vZgaYQ,1204
|
82
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
82
|
+
schemathesis/generation/hypothesis/builder.py,sha256=K8pA5hFkx7sXSFIR8zX7ltYCwJHx3CFcq4Rm12ou1wk,24650
|
83
83
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
84
84
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
85
85
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
86
86
|
schemathesis/generation/hypothesis/strategies.py,sha256=RurE81E06d99YKG48dizy9346ayfNswYTt38zewmGgw,483
|
87
87
|
schemathesis/generation/stateful/__init__.py,sha256=kXpCGbo1-QqfR2N0Z07tLw0Z5_tvbuG3Tk-WI_I1doI,653
|
88
|
-
schemathesis/generation/stateful/state_machine.py,sha256=
|
88
|
+
schemathesis/generation/stateful/state_machine.py,sha256=S-mTOyLEeiO3PMv9LfSxQWSi8KxD5URptDxRpkO5Fmw,10947
|
89
89
|
schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
|
90
90
|
schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
|
91
91
|
schemathesis/graphql/loaders.py,sha256=96R_On1jFvsNuLwqXnO3_TTpsYhdCv0LAmR5jWRXXnY,4756
|
@@ -112,18 +112,18 @@ schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzV
|
|
112
112
|
schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
|
113
113
|
schemathesis/specs/openapi/_cache.py,sha256=HpglmETmZU0RCHxp3DO_sg5_B_nzi54Zuw9vGzzYCxY,4295
|
114
114
|
schemathesis/specs/openapi/_hypothesis.py,sha256=n_39iyz1rt2EdSe-Lyr-3sOIEyJIthnCVR4tGUUvH1c,21328
|
115
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
115
|
+
schemathesis/specs/openapi/checks.py,sha256=R-vehTWC6ZcVYVfKiXVdYHnZBTosMpZQzPPBsJ2dt-Y,26909
|
116
116
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
117
117
|
schemathesis/specs/openapi/converter.py,sha256=lil8IewM5j8tvt4lpA9g_KITvIwx1M96i45DNSHNjoc,3505
|
118
118
|
schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
|
119
119
|
schemathesis/specs/openapi/examples.py,sha256=Uy6naFBq-m1vo_18j4KuZBUYc9qKrBk19jBCWT7tnRg,20464
|
120
120
|
schemathesis/specs/openapi/formats.py,sha256=ViVF3aFeFI1ctwGQbiRDXhU3so82P0BCaF2aDDbUUm8,2816
|
121
|
-
schemathesis/specs/openapi/links.py,sha256=
|
121
|
+
schemathesis/specs/openapi/links.py,sha256=eVlZVnlIcBUSSnYEEb_Bes3tfY5F_BNsOo0IZCBb-io,9452
|
122
122
|
schemathesis/specs/openapi/media_types.py,sha256=ADedOaNWjbAtAekyaKmNj9fY6zBTeqcNqBEjN0EWNhI,1014
|
123
123
|
schemathesis/specs/openapi/parameters.py,sha256=fmOkH-KhMVzWmE6eSw2pw2hernril5MDL7DwwEG4344,14655
|
124
|
-
schemathesis/specs/openapi/patterns.py,sha256=
|
124
|
+
schemathesis/specs/openapi/patterns.py,sha256=6qNZRYQkHtJ98_JMjwhqIGpeR4aR7rlxcCmr1nOHMzk,11258
|
125
125
|
schemathesis/specs/openapi/references.py,sha256=YjD1xMlaYS7xLt6PrrVS20R72ZWHuFZFTa8Llzf54Rg,8808
|
126
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
126
|
+
schemathesis/specs/openapi/schemas.py,sha256=UAukxlH2bVlpZNnx_4EzhDefPpsEtC2ieWz_x336-SQ,54906
|
127
127
|
schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
|
128
128
|
schemathesis/specs/openapi/serialization.py,sha256=JFxMqCr8YWwPT4BVrbvVytcAmkzGXyL1_Q1xR1JKBPs,11464
|
129
129
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
@@ -137,15 +137,16 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=60QqVBTXPTsAojcf7GDs7v8Wb
|
|
137
137
|
schemathesis/specs/openapi/negative/mutations.py,sha256=7jTjD9rt5vxWSVBL5Hx8Avj4WhTA63frDQiFMKysrUU,19248
|
138
138
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
139
139
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
140
|
-
schemathesis/specs/openapi/stateful/__init__.py,sha256=
|
140
|
+
schemathesis/specs/openapi/stateful/__init__.py,sha256=Tk9ahg6VVXwVw8yfFmrz9xhDWskBUaKEeBobdtd44HU,12020
|
141
|
+
schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
|
141
142
|
schemathesis/transport/__init__.py,sha256=z-mRNSOlMBKwQyaEIhpmYv0plWTmK5dJqc9UmQOry80,3949
|
142
143
|
schemathesis/transport/asgi.py,sha256=qTClt6oT_xUEWnRHokACN_uqCNNUZrRPT6YG0PjbElY,926
|
143
144
|
schemathesis/transport/prepare.py,sha256=qQ6zXBw5NN2AIM0bzLAc5Ryc3dmMb0R6xN14lnR49pU,3826
|
144
145
|
schemathesis/transport/requests.py,sha256=OObRvcTL72-BZ7AfuDUrZZU9nZtfBqr22oF8nkzaOLE,8389
|
145
146
|
schemathesis/transport/serialization.py,sha256=jIMra1LqRGav0OX3Hx7mvORt38ll4cd2DKit2D58FN0,10531
|
146
147
|
schemathesis/transport/wsgi.py,sha256=RWSuUXPrl91GxAy8a4jyNNozOWVMRBxKx_tljlWA_Lo,5697
|
147
|
-
schemathesis-4.0.
|
148
|
-
schemathesis-4.0.
|
149
|
-
schemathesis-4.0.
|
150
|
-
schemathesis-4.0.
|
151
|
-
schemathesis-4.0.
|
148
|
+
schemathesis-4.0.0a3.dist-info/METADATA,sha256=80kE1Tg6m91KKgpuMEUsuPEPYJzCxeXcGthSlW1_BYs,12292
|
149
|
+
schemathesis-4.0.0a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
150
|
+
schemathesis-4.0.0a3.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
151
|
+
schemathesis-4.0.0a3.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
152
|
+
schemathesis-4.0.0a3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|