schemathesis 4.3.10__py3-none-any.whl → 4.3.12__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.
Potentially problematic release.
This version of schemathesis might be problematic. Click here for more details.
- schemathesis/cli/commands/__init__.py +6 -1
- schemathesis/cli/commands/run/loaders.py +5 -1
- schemathesis/config/__init__.py +6 -2
- schemathesis/generation/coverage.py +1 -2
- schemathesis/generation/hypothesis/builder.py +2 -4
- schemathesis/generation/stateful/state_machine.py +10 -5
- schemathesis/openapi/loaders.py +5 -4
- schemathesis/schemas.py +4 -3
- schemathesis/specs/openapi/adapter/security.py +1 -1
- schemathesis/specs/openapi/examples.py +24 -12
- schemathesis/specs/openapi/stateful/__init__.py +80 -34
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +12 -1
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +117 -1
- schemathesis/specs/openapi/stateful/dependencies/resources.py +16 -0
- schemathesis/specs/openapi/stateful/links.py +13 -4
- {schemathesis-4.3.10.dist-info → schemathesis-4.3.12.dist-info}/METADATA +2 -2
- {schemathesis-4.3.10.dist-info → schemathesis-4.3.12.dist-info}/RECORD +20 -21
- schemathesis/generation/hypothesis/strategies.py +0 -16
- {schemathesis-4.3.10.dist-info → schemathesis-4.3.12.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.10.dist-info → schemathesis-4.3.12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.10.dist-info → schemathesis-4.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
|
-
from tomli import TOMLDecodeError
|
|
8
8
|
|
|
9
9
|
from schemathesis.cli.commands.data import Data
|
|
10
10
|
from schemathesis.cli.commands.run import run as run_command
|
|
@@ -16,6 +16,11 @@ from schemathesis.config import ConfigError, SchemathesisConfig
|
|
|
16
16
|
from schemathesis.core.errors import HookError, format_exception
|
|
17
17
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
|
18
18
|
|
|
19
|
+
if sys.version_info < (3, 11):
|
|
20
|
+
from tomli import TOMLDecodeError
|
|
21
|
+
else:
|
|
22
|
+
from tomllib import TOMLDecodeError
|
|
23
|
+
|
|
19
24
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
20
25
|
|
|
21
26
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import warnings
|
|
11
|
+
from json import JSONDecodeError
|
|
11
12
|
from typing import TYPE_CHECKING, Any, Callable
|
|
12
13
|
|
|
13
14
|
from schemathesis import graphql, openapi
|
|
@@ -35,7 +36,10 @@ def should_try_more(exc: LoaderError) -> bool:
|
|
|
35
36
|
import requests
|
|
36
37
|
from yaml.reader import ReaderError
|
|
37
38
|
|
|
38
|
-
if isinstance(exc.__cause__, ReaderError) and "characters are not allowed" in str(exc.__cause__)
|
|
39
|
+
if (isinstance(exc.__cause__, ReaderError) and "characters are not allowed" in str(exc.__cause__)) or (
|
|
40
|
+
isinstance(exc.__cause__, JSONDecodeError)
|
|
41
|
+
and ('"swagger"' in exc.__cause__.doc or '"openapi"' in exc.__cause__.doc)
|
|
42
|
+
):
|
|
39
43
|
return False
|
|
40
44
|
|
|
41
45
|
# We should not try other loaders for cases when we can't even establish connection
|
schemathesis/config/__init__.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from os import PathLike
|
|
6
7
|
from random import Random
|
|
7
8
|
|
|
8
|
-
import tomli
|
|
9
|
-
|
|
10
9
|
from schemathesis.config._checks import (
|
|
11
10
|
CheckConfig,
|
|
12
11
|
ChecksConfig,
|
|
@@ -29,6 +28,11 @@ from schemathesis.config._phases import (
|
|
|
29
28
|
from schemathesis.config._projects import ProjectConfig, ProjectsConfig, SchemathesisWarning, get_workers_count
|
|
30
29
|
from schemathesis.config._report import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat, ReportsConfig
|
|
31
30
|
|
|
31
|
+
if sys.version_info < (3, 11):
|
|
32
|
+
import tomli
|
|
33
|
+
else:
|
|
34
|
+
import tomllib as tomli
|
|
35
|
+
|
|
32
36
|
__all__ = [
|
|
33
37
|
"SchemathesisConfig",
|
|
34
38
|
"ConfigError",
|
|
@@ -9,7 +9,6 @@ from itertools import combinations
|
|
|
9
9
|
|
|
10
10
|
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
|
11
11
|
from schemathesis.core.jsonschema.keywords import ALL_KEYWORDS
|
|
12
|
-
from schemathesis.generation.hypothesis.strategies import combine
|
|
13
12
|
|
|
14
13
|
try:
|
|
15
14
|
from json.encoder import _make_iterencode # type: ignore[attr-defined]
|
|
@@ -81,7 +80,7 @@ STRATEGIES_FOR_TYPE = {
|
|
|
81
80
|
def get_strategy_for_type(ty: str | list[str]) -> st.SearchStrategy:
|
|
82
81
|
if isinstance(ty, str):
|
|
83
82
|
return STRATEGIES_FOR_TYPE[ty]
|
|
84
|
-
return
|
|
83
|
+
return st.one_of(STRATEGIES_FOR_TYPE[t] for t in ty if t in STRATEGIES_FOR_TYPE)
|
|
85
84
|
|
|
86
85
|
|
|
87
86
|
FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
|
|
@@ -35,7 +35,7 @@ from schemathesis.core.transport import prepare_urlencoded
|
|
|
35
35
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
|
36
36
|
from schemathesis.generation import GenerationMode, coverage
|
|
37
37
|
from schemathesis.generation.case import Case
|
|
38
|
-
from schemathesis.generation.hypothesis import examples, setup
|
|
38
|
+
from schemathesis.generation.hypothesis import examples, setup
|
|
39
39
|
from schemathesis.generation.hypothesis.examples import add_single_example
|
|
40
40
|
from schemathesis.generation.hypothesis.given import GivenInput
|
|
41
41
|
from schemathesis.generation.meta import (
|
|
@@ -112,9 +112,7 @@ def create_test(
|
|
|
112
112
|
**config.as_strategy_kwargs,
|
|
113
113
|
}
|
|
114
114
|
generation = config.project.generation_for(operation=operation)
|
|
115
|
-
strategy =
|
|
116
|
-
[operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
|
|
117
|
-
)
|
|
115
|
+
strategy = st.one_of(operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes)
|
|
118
116
|
|
|
119
117
|
hypothesis_test = create_base_test(
|
|
120
118
|
test_function=test_func,
|
|
@@ -37,16 +37,20 @@ class StepInput:
|
|
|
37
37
|
|
|
38
38
|
case: Case
|
|
39
39
|
transition: Transition | None # None for initial steps
|
|
40
|
-
#
|
|
40
|
+
# What parameters were actually applied
|
|
41
41
|
# Data extraction failures can prevent it, as well as transitions can be skipped in some cases
|
|
42
42
|
# to improve discovery of bugs triggered by non-stateful inputs during stateful testing
|
|
43
|
-
|
|
43
|
+
applied_parameters: list[str]
|
|
44
44
|
|
|
45
|
-
__slots__ = ("case", "transition", "
|
|
45
|
+
__slots__ = ("case", "transition", "applied_parameters")
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
48
48
|
def initial(cls, case: Case) -> StepInput:
|
|
49
|
-
return cls(case=case, transition=None,
|
|
49
|
+
return cls(case=case, transition=None, applied_parameters=[])
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_applied(self) -> bool:
|
|
53
|
+
return bool(self.applied_parameters)
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
@dataclass
|
|
@@ -69,8 +73,9 @@ class ExtractedParam:
|
|
|
69
73
|
|
|
70
74
|
definition: Any
|
|
71
75
|
value: Result[Any, Exception]
|
|
76
|
+
is_required: bool
|
|
72
77
|
|
|
73
|
-
__slots__ = ("definition", "value")
|
|
78
|
+
__slots__ = ("definition", "value", "is_required")
|
|
74
79
|
|
|
75
80
|
|
|
76
81
|
@dataclass
|
schemathesis/openapi/loaders.py
CHANGED
|
@@ -303,10 +303,11 @@ def _load_yaml(content: str) -> dict[str, Any]:
|
|
|
303
303
|
try:
|
|
304
304
|
return deserialize_yaml(content)
|
|
305
305
|
except yaml.YAMLError as exc:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
raise LoaderError(
|
|
307
|
+
LoaderErrorKind.SYNTAX_ERROR,
|
|
308
|
+
SCHEMA_SYNTAX_ERROR,
|
|
309
|
+
extras=[entry for entry in str(exc).splitlines() if entry],
|
|
310
|
+
) from exc
|
|
310
311
|
|
|
311
312
|
|
|
312
313
|
SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
|
schemathesis/schemas.py
CHANGED
|
@@ -16,6 +16,8 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
|
18
18
|
|
|
19
|
+
from hypothesis import strategies as st
|
|
20
|
+
|
|
19
21
|
from schemathesis import transport
|
|
20
22
|
from schemathesis.config import ProjectConfig
|
|
21
23
|
from schemathesis.core import NOT_SET, NotSet, media_types
|
|
@@ -25,7 +27,6 @@ from schemathesis.core.result import Ok, Result
|
|
|
25
27
|
from schemathesis.core.transport import Response
|
|
26
28
|
from schemathesis.generation import GenerationMode
|
|
27
29
|
from schemathesis.generation.case import Case
|
|
28
|
-
from schemathesis.generation.hypothesis import strategies
|
|
29
30
|
from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
|
|
30
31
|
from schemathesis.generation.meta import CaseMetadata
|
|
31
32
|
from schemathesis.hooks import HookDispatcherMark, _should_skip_hook
|
|
@@ -458,7 +459,7 @@ class BaseSchema(Mapping):
|
|
|
458
459
|
for operation in self.get_all_operations()
|
|
459
460
|
if isinstance(operation, Ok)
|
|
460
461
|
]
|
|
461
|
-
return
|
|
462
|
+
return st.one_of(_strategies)
|
|
462
463
|
|
|
463
464
|
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
|
464
465
|
raise NotImplementedError
|
|
@@ -500,7 +501,7 @@ class APIOperationMap(Mapping):
|
|
|
500
501
|
_strategies = [
|
|
501
502
|
operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
|
|
502
503
|
]
|
|
503
|
-
return
|
|
504
|
+
return st.one_of(_strategies)
|
|
504
505
|
|
|
505
506
|
|
|
506
507
|
P = TypeVar("P", bound=OperationParameter)
|
|
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from schemathesis.core.compat import RefResolver
|
|
8
8
|
from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
|
|
9
9
|
|
|
10
|
-
ORIGINAL_SECURITY_TYPE_KEY = "x-original-
|
|
10
|
+
ORIGINAL_SECURITY_TYPE_KEY = "x-original-security-type"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -12,7 +12,6 @@ from hypothesis_jsonschema import from_schema
|
|
|
12
12
|
from schemathesis.config import GenerationConfig
|
|
13
13
|
from schemathesis.core.compat import RefResolutionError, RefResolver
|
|
14
14
|
from schemathesis.core.errors import InfiniteRecursiveReference, UnresolvableReference
|
|
15
|
-
from schemathesis.core.jsonschema import references
|
|
16
15
|
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
|
17
16
|
from schemathesis.core.transforms import deepclone
|
|
18
17
|
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
|
@@ -216,11 +215,11 @@ def _resolve_bundled(
|
|
|
216
215
|
if isinstance(reference, str):
|
|
217
216
|
# Check if this reference is already in the current path
|
|
218
217
|
if reference in reference_path:
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
# Real infinite recursive references are caught at the bundling stage.
|
|
219
|
+
# This recursion happens because of how the example phase generates data - it explores everything,
|
|
220
|
+
# so it is the easiest way to break such cycles
|
|
221
|
+
cycle = list(reference_path[reference_path.index(reference) :])
|
|
222
|
+
raise InfiniteRecursiveReference(reference, cycle)
|
|
224
223
|
|
|
225
224
|
new_path = reference_path + (reference,)
|
|
226
225
|
|
|
@@ -238,7 +237,11 @@ def _expand_subschemas(
|
|
|
238
237
|
*, schema: dict[str, Any] | bool, resolver: RefResolver, reference_path: tuple[str, ...]
|
|
239
238
|
) -> Generator[tuple[dict[str, Any] | bool, tuple[str, ...]], None, None]:
|
|
240
239
|
"""Expand schema and all its subschemas."""
|
|
241
|
-
|
|
240
|
+
try:
|
|
241
|
+
schema, current_path = _resolve_bundled(schema, resolver, reference_path)
|
|
242
|
+
except InfiniteRecursiveReference:
|
|
243
|
+
return
|
|
244
|
+
|
|
242
245
|
yield (schema, current_path)
|
|
243
246
|
|
|
244
247
|
if isinstance(schema, dict):
|
|
@@ -250,13 +253,19 @@ def _expand_subschemas(
|
|
|
250
253
|
yield (subschema, current_path)
|
|
251
254
|
|
|
252
255
|
# For allOf, merge all alternatives
|
|
253
|
-
if "allOf" in schema:
|
|
256
|
+
if "allOf" in schema and schema["allOf"]:
|
|
254
257
|
subschema = deepclone(schema["allOf"][0])
|
|
255
|
-
|
|
258
|
+
try:
|
|
259
|
+
subschema, expanded_path = _resolve_bundled(subschema, resolver, current_path)
|
|
260
|
+
except InfiniteRecursiveReference:
|
|
261
|
+
return
|
|
256
262
|
|
|
257
263
|
for sub in schema["allOf"][1:]:
|
|
258
264
|
if isinstance(sub, dict):
|
|
259
|
-
|
|
265
|
+
try:
|
|
266
|
+
sub, _ = _resolve_bundled(sub, resolver, current_path)
|
|
267
|
+
except InfiniteRecursiveReference:
|
|
268
|
+
return
|
|
260
269
|
for key, value in sub.items():
|
|
261
270
|
if key == "properties":
|
|
262
271
|
subschema.setdefault("properties", {}).update(value)
|
|
@@ -269,7 +278,7 @@ def _expand_subschemas(
|
|
|
269
278
|
else:
|
|
270
279
|
subschema[key] = value
|
|
271
280
|
|
|
272
|
-
yield (subschema,
|
|
281
|
+
yield (subschema, expanded_path)
|
|
273
282
|
|
|
274
283
|
|
|
275
284
|
def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
|
|
@@ -353,7 +362,10 @@ def extract_from_schema(
|
|
|
353
362
|
) -> Generator[Any, None, None]:
|
|
354
363
|
"""Extract all examples from a single schema definition."""
|
|
355
364
|
# This implementation supports only `properties` and `items`
|
|
356
|
-
|
|
365
|
+
try:
|
|
366
|
+
schema, current_path = _resolve_bundled(schema, resolver, reference_path)
|
|
367
|
+
except InfiniteRecursiveReference:
|
|
368
|
+
return
|
|
357
369
|
|
|
358
370
|
if "properties" in schema:
|
|
359
371
|
variants = {}
|
|
@@ -8,6 +8,7 @@ import jsonschema
|
|
|
8
8
|
from hypothesis import strategies as st
|
|
9
9
|
from hypothesis.stateful import Bundle, Rule, precondition, rule
|
|
10
10
|
|
|
11
|
+
from schemathesis.core import NOT_SET
|
|
11
12
|
from schemathesis.core.errors import InvalidStateMachine, InvalidTransition
|
|
12
13
|
from schemathesis.core.parameters import ParameterLocation
|
|
13
14
|
from schemathesis.core.result import Ok
|
|
@@ -15,7 +16,6 @@ from schemathesis.core.transforms import UNRESOLVABLE
|
|
|
15
16
|
from schemathesis.engine.recorder import ScenarioRecorder
|
|
16
17
|
from schemathesis.generation import GenerationMode
|
|
17
18
|
from schemathesis.generation.case import Case
|
|
18
|
-
from schemathesis.generation.hypothesis import strategies
|
|
19
19
|
from schemathesis.generation.meta import ComponentInfo, TestPhase
|
|
20
20
|
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
|
21
21
|
from schemathesis.generation.stateful.state_machine import APIStateMachine, StepInput, StepOutput, _normalize_name
|
|
@@ -50,7 +50,7 @@ class OpenAPIStateMachine(APIStateMachine):
|
|
|
50
50
|
# The proportion of negative tests generated for "root" transitions
|
|
51
51
|
NEGATIVE_TEST_CASES_THRESHOLD = 10
|
|
52
52
|
# How often some transition is skipped
|
|
53
|
-
|
|
53
|
+
BASE_EXPLORATION_RATE = 0.15
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
@dataclass
|
|
@@ -163,6 +163,7 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
163
163
|
for target in operations:
|
|
164
164
|
if target.label in transitions.operations:
|
|
165
165
|
incoming = transitions.operations[target.label].incoming
|
|
166
|
+
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
166
167
|
if incoming:
|
|
167
168
|
for link in incoming:
|
|
168
169
|
bundle_name = f"{link.source.label} -> {link.status_code}"
|
|
@@ -170,7 +171,6 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
170
171
|
f"{link.source.label} -> {link.status_code} -> {link.name} -> {target.label}"
|
|
171
172
|
)
|
|
172
173
|
assert name not in rules, name
|
|
173
|
-
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
174
174
|
rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
|
|
175
175
|
transition(
|
|
176
176
|
name=name,
|
|
@@ -182,7 +182,6 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
182
182
|
)
|
|
183
183
|
if target.label in roots.reliable or (not roots.reliable and target.label in roots.fallback):
|
|
184
184
|
name = _normalize_name(f"RANDOM -> {target.label}")
|
|
185
|
-
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
186
185
|
if len(config.modes) == 1:
|
|
187
186
|
case_strategy = target.as_strategy(generation_mode=config.modes[0], phase=TestPhase.STATEFUL)
|
|
188
187
|
else:
|
|
@@ -250,50 +249,97 @@ def is_likely_root_transition(operation: APIOperation) -> bool:
|
|
|
250
249
|
|
|
251
250
|
|
|
252
251
|
def into_step_input(
|
|
253
|
-
target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
|
|
252
|
+
*, target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
|
|
254
253
|
) -> Callable[[StepOutput], st.SearchStrategy[StepInput]]:
|
|
254
|
+
"""A single transition between API operations."""
|
|
255
|
+
|
|
255
256
|
def builder(_output: StepOutput) -> st.SearchStrategy[StepInput]:
|
|
256
257
|
@st.composite # type: ignore[misc]
|
|
257
258
|
def inner(draw: st.DrawFn, output: StepOutput) -> StepInput:
|
|
259
|
+
random = draw(st.randoms(use_true_random=True))
|
|
260
|
+
|
|
261
|
+
def biased_coin(p: float) -> bool:
|
|
262
|
+
return random.random() < p
|
|
263
|
+
|
|
264
|
+
# Extract transition data from previous operation's output
|
|
258
265
|
transition = link.extract(output)
|
|
259
266
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if isinstance(extracted.value, Ok) and extracted.value.ok() not in (None, UNRESOLVABLE)
|
|
265
|
-
}
|
|
266
|
-
for container, data in transition.parameters.items()
|
|
267
|
-
}
|
|
267
|
+
overrides: dict[str, Any] = {}
|
|
268
|
+
applied_parameters = []
|
|
269
|
+
for container, data in transition.parameters.items():
|
|
270
|
+
overrides[container] = {}
|
|
268
271
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
and not link.merge_body
|
|
274
|
-
and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
|
|
275
|
-
):
|
|
276
|
-
kwargs["body"] = transition.request_body.value.ok()
|
|
272
|
+
for name, extracted in data.items():
|
|
273
|
+
# Skip if extraction failed or returned unusable value
|
|
274
|
+
if not isinstance(extracted.value, Ok) or extracted.value.ok() in (None, UNRESOLVABLE):
|
|
275
|
+
continue
|
|
277
276
|
|
|
278
|
-
|
|
277
|
+
param_key = f"{container}.{name}"
|
|
279
278
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
# Calculate exploration rate based on parameter characteristics
|
|
280
|
+
exploration_rate = BASE_EXPLORATION_RATE
|
|
281
|
+
|
|
282
|
+
# Path parameters are critical for routing - use link values more often
|
|
283
|
+
if container == "path_parameters":
|
|
284
|
+
exploration_rate *= 0.5
|
|
285
|
+
|
|
286
|
+
# Required parameters should follow links more often, optional ones explored more
|
|
287
|
+
# Path params are always required, so they get both multipliers
|
|
288
|
+
if extracted.is_required:
|
|
289
|
+
exploration_rate *= 0.5
|
|
290
|
+
else:
|
|
291
|
+
# Explore optional parameters more to avoid only testing link-provided values
|
|
292
|
+
exploration_rate *= 3.0
|
|
293
|
+
|
|
294
|
+
if biased_coin(1 - exploration_rate):
|
|
295
|
+
overrides[container][name] = extracted.value.ok()
|
|
296
|
+
applied_parameters.append(param_key)
|
|
297
|
+
|
|
298
|
+
# Get the extracted body value
|
|
284
299
|
if (
|
|
285
300
|
transition.request_body is not None
|
|
286
301
|
and isinstance(transition.request_body.value, Ok)
|
|
287
302
|
and transition.request_body.value.ok() is not UNRESOLVABLE
|
|
288
|
-
and link.merge_body
|
|
289
|
-
and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
|
|
290
303
|
):
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
304
|
+
request_body = transition.request_body.value.ok()
|
|
305
|
+
else:
|
|
306
|
+
request_body = NOT_SET
|
|
307
|
+
|
|
308
|
+
# Link suppose to replace the entire extracted body
|
|
309
|
+
if request_body is not NOT_SET and not link.merge_body and biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
310
|
+
overrides["body"] = request_body
|
|
311
|
+
if isinstance(overrides["body"], dict):
|
|
312
|
+
applied_parameters.extend(f"body.{field}" for field in overrides["body"])
|
|
294
313
|
else:
|
|
295
|
-
|
|
296
|
-
|
|
314
|
+
applied_parameters.append("body")
|
|
315
|
+
|
|
316
|
+
cases = st.one_of(
|
|
317
|
+
[target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **overrides) for mode in modes]
|
|
318
|
+
)
|
|
319
|
+
case = draw(cases)
|
|
320
|
+
if request_body is not NOT_SET and link.merge_body:
|
|
321
|
+
if isinstance(request_body, dict):
|
|
322
|
+
selected_fields = {}
|
|
323
|
+
|
|
324
|
+
for field_name, field_value in request_body.items():
|
|
325
|
+
if field_value is UNRESOLVABLE:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
329
|
+
selected_fields[field_name] = field_value
|
|
330
|
+
applied_parameters.append(f"body.{field_name}")
|
|
331
|
+
|
|
332
|
+
if selected_fields:
|
|
333
|
+
if isinstance(case.body, dict):
|
|
334
|
+
case.body = {**case.body, **selected_fields}
|
|
335
|
+
else:
|
|
336
|
+
# Can't merge into non-dict, replace entirely
|
|
337
|
+
case.body = selected_fields
|
|
338
|
+
elif biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
339
|
+
case.body = request_body
|
|
340
|
+
applied_parameters.append("body")
|
|
341
|
+
|
|
342
|
+
# Re-validate generation mode after merging body
|
|
297
343
|
if case.meta and case.meta.generation.mode == GenerationMode.NEGATIVE:
|
|
298
344
|
# It is possible that the new body is now valid and the whole test case could be valid too
|
|
299
345
|
for alternative in case.operation.body:
|
|
@@ -305,7 +351,7 @@ def into_step_input(
|
|
|
305
351
|
)
|
|
306
352
|
if all(info.mode == GenerationMode.POSITIVE for info in case.meta.components.values()):
|
|
307
353
|
case.meta.generation.mode = GenerationMode.POSITIVE
|
|
308
|
-
return StepInput(case=case, transition=transition,
|
|
354
|
+
return StepInput(case=case, transition=transition, applied_parameters=applied_parameters)
|
|
309
355
|
|
|
310
356
|
return inner(output=_output)
|
|
311
357
|
|
|
@@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any
|
|
|
10
10
|
from schemathesis.core import NOT_SET
|
|
11
11
|
from schemathesis.core.compat import RefResolutionError
|
|
12
12
|
from schemathesis.core.result import Ok
|
|
13
|
-
from schemathesis.specs.openapi.stateful.dependencies.inputs import
|
|
13
|
+
from schemathesis.specs.openapi.stateful.dependencies.inputs import (
|
|
14
|
+
extract_inputs,
|
|
15
|
+
merge_related_resources,
|
|
16
|
+
update_input_field_bindings,
|
|
17
|
+
)
|
|
14
18
|
from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
15
19
|
CanonicalizationCache,
|
|
16
20
|
Cardinality,
|
|
@@ -25,6 +29,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
25
29
|
ResourceMap,
|
|
26
30
|
)
|
|
27
31
|
from schemathesis.specs.openapi.stateful.dependencies.outputs import extract_outputs
|
|
32
|
+
from schemathesis.specs.openapi.stateful.dependencies.resources import remove_unused_resources
|
|
28
33
|
|
|
29
34
|
if TYPE_CHECKING:
|
|
30
35
|
from schemathesis.schemas import APIOperation
|
|
@@ -88,6 +93,12 @@ def analyze(schema: BaseOpenAPISchema) -> DependencyGraph:
|
|
|
88
93
|
for resource in updated_resources:
|
|
89
94
|
update_input_field_bindings(resource, operations)
|
|
90
95
|
|
|
96
|
+
# Merge parameter-inferred resources with schema-defined ones
|
|
97
|
+
merge_related_resources(operations, resources)
|
|
98
|
+
|
|
99
|
+
# Clean up orphaned resources
|
|
100
|
+
remove_unused_resources(operations, resources)
|
|
101
|
+
|
|
91
102
|
return DependencyGraph(operations=operations, resources=resources)
|
|
92
103
|
|
|
93
104
|
|
|
@@ -14,6 +14,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
14
14
|
DefinitionSource,
|
|
15
15
|
InputSlot,
|
|
16
16
|
OperationMap,
|
|
17
|
+
OutputSlot,
|
|
17
18
|
ResourceDefinition,
|
|
18
19
|
ResourceMap,
|
|
19
20
|
)
|
|
@@ -39,7 +40,7 @@ def extract_inputs(
|
|
|
39
40
|
creating placeholder resources if not yet discovered from their schemas.
|
|
40
41
|
"""
|
|
41
42
|
known_dependencies = set()
|
|
42
|
-
for param in operation.
|
|
43
|
+
for param in operation.iter_parameters():
|
|
43
44
|
input_slot = _resolve_parameter_dependency(
|
|
44
45
|
parameter_name=param.name,
|
|
45
46
|
parameter_location=param.location,
|
|
@@ -310,3 +311,118 @@ def update_input_field_bindings(resource_name: str, operations: OperationMap) ->
|
|
|
310
311
|
)
|
|
311
312
|
if new_field is not None:
|
|
312
313
|
input_slot.resource_field = new_field
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def merge_related_resources(operations: OperationMap, resources: ResourceMap) -> None:
|
|
317
|
+
"""Merge parameter-inferred resources with schema-defined resources from related operations."""
|
|
318
|
+
candidates = find_producer_consumer_candidates(operations)
|
|
319
|
+
|
|
320
|
+
for producer_name, consumer_name in candidates:
|
|
321
|
+
producer = operations[producer_name]
|
|
322
|
+
consumer = operations[consumer_name]
|
|
323
|
+
|
|
324
|
+
# Try to upgrade each input slot
|
|
325
|
+
for input_slot in consumer.inputs:
|
|
326
|
+
result = try_merge_input_resource(input_slot, producer.outputs, resources)
|
|
327
|
+
|
|
328
|
+
if result is not None:
|
|
329
|
+
new_resource_name, new_field_name = result
|
|
330
|
+
# Update input slot to use the better resource definition
|
|
331
|
+
input_slot.resource = resources[new_resource_name]
|
|
332
|
+
input_slot.resource_field = new_field_name
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def try_merge_input_resource(
|
|
336
|
+
input_slot: InputSlot,
|
|
337
|
+
producer_outputs: list[OutputSlot],
|
|
338
|
+
resources: ResourceMap,
|
|
339
|
+
) -> tuple[str, str] | None:
|
|
340
|
+
"""Try to upgrade an input's resource to a producer's resource."""
|
|
341
|
+
consumer_resource = input_slot.resource
|
|
342
|
+
|
|
343
|
+
# Only upgrade parameter-inferred resources (low confidence)
|
|
344
|
+
if consumer_resource.source != DefinitionSource.PARAMETER_INFERENCE:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# Try each producer output
|
|
348
|
+
for output in producer_outputs:
|
|
349
|
+
producer_resource = resources[output.resource.name]
|
|
350
|
+
|
|
351
|
+
# Only merge to schema-defined resources (high confidence)
|
|
352
|
+
if producer_resource.source != DefinitionSource.SCHEMA_WITH_PROPERTIES:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Try to match the input parameter to producer's fields
|
|
356
|
+
param_name = input_slot.parameter_name
|
|
357
|
+
if not isinstance(param_name, str):
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
for resource_name in (input_slot.resource.name, producer_resource.name):
|
|
361
|
+
matched_field = naming.find_matching_field(
|
|
362
|
+
parameter=param_name,
|
|
363
|
+
resource=resource_name,
|
|
364
|
+
fields=producer_resource.fields,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if matched_field is not None:
|
|
368
|
+
return (producer_resource.name, matched_field)
|
|
369
|
+
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def find_producer_consumer_candidates(operations: OperationMap) -> list[tuple[str, str]]:
|
|
374
|
+
"""Find operation pairs that might produce/consume the same resource via REST patterns."""
|
|
375
|
+
candidates = []
|
|
376
|
+
|
|
377
|
+
# Group by base path to reduce comparisons
|
|
378
|
+
paths: dict[str, list[str]] = {}
|
|
379
|
+
for name, node in operations.items():
|
|
380
|
+
base = _extract_base_path(node.path)
|
|
381
|
+
paths.setdefault(base, []).append(name)
|
|
382
|
+
|
|
383
|
+
# Within each path group, find POST/PUT → GET/DELETE/PATCH patterns
|
|
384
|
+
for names in paths.values():
|
|
385
|
+
for producer_name in names:
|
|
386
|
+
producer = operations[producer_name]
|
|
387
|
+
# Producer must create/update and return data
|
|
388
|
+
if producer.method not in ("post", "put") or not producer.outputs:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
for consumer_name in names:
|
|
392
|
+
consumer = operations[consumer_name]
|
|
393
|
+
# Consumer must have path parameters
|
|
394
|
+
if not consumer.inputs:
|
|
395
|
+
continue
|
|
396
|
+
# Paths must be related (collection + item pattern)
|
|
397
|
+
if _is_collection_item_pattern(producer.path, consumer.path):
|
|
398
|
+
candidates.append((producer_name, consumer_name))
|
|
399
|
+
|
|
400
|
+
return candidates
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _extract_base_path(path: str) -> str:
|
|
404
|
+
"""Extract collection path: /blog/posts/{id} -> /blog/posts."""
|
|
405
|
+
parts = [p for p in path.split("/") if not p.startswith("{")]
|
|
406
|
+
return "/".join(parts).rstrip("/")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _is_collection_item_pattern(collection_path: str, item_path: str) -> bool:
|
|
410
|
+
"""Check if paths follow REST collection/item pattern."""
|
|
411
|
+
# /blog/posts + /blog/posts/{postId}
|
|
412
|
+
normalized_collection = collection_path.rstrip("/")
|
|
413
|
+
normalized_item = item_path.rstrip("/")
|
|
414
|
+
|
|
415
|
+
# Must start with collection path
|
|
416
|
+
if not normalized_item.startswith(normalized_collection + "/"):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
# Extract the segment after collection path
|
|
420
|
+
remainder = normalized_item[len(normalized_collection) + 1 :]
|
|
421
|
+
|
|
422
|
+
# Must be a single path parameter: {paramName} with no slashes
|
|
423
|
+
return (
|
|
424
|
+
remainder.startswith("{")
|
|
425
|
+
and remainder.endswith("}")
|
|
426
|
+
and len(remainder) > 2 # Not empty {}
|
|
427
|
+
and "/" not in remainder
|
|
428
|
+
)
|
|
@@ -13,6 +13,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
13
13
|
CanonicalizationCache,
|
|
14
14
|
Cardinality,
|
|
15
15
|
DefinitionSource,
|
|
16
|
+
OperationMap,
|
|
16
17
|
ResourceDefinition,
|
|
17
18
|
ResourceMap,
|
|
18
19
|
extend_pointer,
|
|
@@ -310,3 +311,18 @@ def _extract_resource_from_schema(
|
|
|
310
311
|
resources[resource_name] = resource
|
|
311
312
|
|
|
312
313
|
return resource
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def remove_unused_resources(operations: OperationMap, resources: ResourceMap) -> None:
|
|
317
|
+
"""Remove resources that aren't referenced by any operation."""
|
|
318
|
+
# Collect all resource names currently in use
|
|
319
|
+
used_resources = set()
|
|
320
|
+
for operation in operations.values():
|
|
321
|
+
for input_slot in operation.inputs:
|
|
322
|
+
used_resources.add(input_slot.resource.name)
|
|
323
|
+
for output_slot in operation.outputs:
|
|
324
|
+
used_resources.add(output_slot.resource.name)
|
|
325
|
+
|
|
326
|
+
unused = set(resources.keys()) - used_resources
|
|
327
|
+
for resource_name in unused:
|
|
328
|
+
del resources[resource_name]
|
|
@@ -23,8 +23,9 @@ class NormalizedParameter:
|
|
|
23
23
|
name: str
|
|
24
24
|
expression: str
|
|
25
25
|
container_name: str
|
|
26
|
+
is_required: bool
|
|
26
27
|
|
|
27
|
-
__slots__ = ("location", "name", "expression", "container_name")
|
|
28
|
+
__slots__ = ("location", "name", "expression", "container_name", "is_required")
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass(repr=False)
|
|
@@ -131,15 +132,21 @@ class OpenApiLink:
|
|
|
131
132
|
except Exception as exc:
|
|
132
133
|
errors.append(TransitionValidationError(str(exc)))
|
|
133
134
|
|
|
135
|
+
is_required = False
|
|
134
136
|
if hasattr(self, "target"):
|
|
135
137
|
try:
|
|
136
138
|
container_name = self._get_parameter_container(location, name)
|
|
137
139
|
except TransitionValidationError as exc:
|
|
138
140
|
errors.append(exc)
|
|
139
141
|
continue
|
|
142
|
+
|
|
143
|
+
for param in self.target.iter_parameters():
|
|
144
|
+
if param.name == name:
|
|
145
|
+
is_required = param.is_required
|
|
146
|
+
break
|
|
140
147
|
else:
|
|
141
148
|
continue
|
|
142
|
-
result.append(NormalizedParameter(location, name, expression, container_name))
|
|
149
|
+
result.append(NormalizedParameter(location, name, expression, container_name, is_required=is_required))
|
|
143
150
|
return result
|
|
144
151
|
|
|
145
152
|
def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
|
|
@@ -178,7 +185,9 @@ class OpenApiLink:
|
|
|
178
185
|
value = Ok(expressions.evaluate(parameter.expression, output))
|
|
179
186
|
except Exception as exc:
|
|
180
187
|
value = Err(exc)
|
|
181
|
-
container[parameter.name] = ExtractedParam(
|
|
188
|
+
container[parameter.name] = ExtractedParam(
|
|
189
|
+
definition=parameter.expression, value=value, is_required=parameter.is_required
|
|
190
|
+
)
|
|
182
191
|
return extracted
|
|
183
192
|
|
|
184
193
|
def extract_body(self, output: StepOutput) -> ExtractedParam | None:
|
|
@@ -188,7 +197,7 @@ class OpenApiLink:
|
|
|
188
197
|
value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
|
|
189
198
|
except Exception as exc:
|
|
190
199
|
value = Err(exc)
|
|
191
|
-
return ExtractedParam(definition=self.body, value=value)
|
|
200
|
+
return ExtractedParam(definition=self.body, value=value, is_required=True)
|
|
192
201
|
return None
|
|
193
202
|
|
|
194
203
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.12
|
|
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
|
|
@@ -46,7 +46,7 @@ Requires-Dist: pyyaml<7.0,>=5.1
|
|
|
46
46
|
Requires-Dist: requests<3,>=2.22
|
|
47
47
|
Requires-Dist: rich>=13.9.4
|
|
48
48
|
Requires-Dist: starlette-testclient<1,>=0.4.1
|
|
49
|
-
Requires-Dist: tomli>=2.2.1
|
|
49
|
+
Requires-Dist: tomli>=2.2.1; python_version < '3.11'
|
|
50
50
|
Requires-Dist: typing-extensions>=4.12.2
|
|
51
51
|
Requires-Dist: werkzeug<4,>=0.16.0
|
|
52
52
|
Provides-Extra: bench
|
|
@@ -5,19 +5,19 @@ schemathesis/errors.py,sha256=K3irHIZkrBH2-9LIjlgXlm8RNC41Nffd39ncfwagUvw,1053
|
|
|
5
5
|
schemathesis/filters.py,sha256=IevPA5A04GfRLLjmkFLZ0CLhjNO3RmpZq_yw6MqjLIA,13515
|
|
6
6
|
schemathesis/hooks.py,sha256=q2wqYNgpMCO8ImSBkbrWDSwN0BSELelqJMgAAgGvv2M,14836
|
|
7
7
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
schemathesis/schemas.py,sha256=
|
|
8
|
+
schemathesis/schemas.py,sha256=Rn4WHRTe1dGVtoHO1c2N7K13THevuWxeAt8wyvMZmy0,27547
|
|
9
9
|
schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,877
|
|
10
10
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
11
11
|
schemathesis/cli/constants.py,sha256=CVcQNHEiX-joAQmyuEVKWPOSxDHsOw_EXXZsEclzLuY,341
|
|
12
12
|
schemathesis/cli/core.py,sha256=ue7YUdVo3YvuzGL4s6i62NL6YqNDeVPBSnQ1znrvG2w,480
|
|
13
|
-
schemathesis/cli/commands/__init__.py,sha256=
|
|
13
|
+
schemathesis/cli/commands/__init__.py,sha256=cZowfGwoby-2LmOS6ViRXjSTHe--vaTLndIbXbwF188,3833
|
|
14
14
|
schemathesis/cli/commands/data.py,sha256=_ALywjIeCZjuaoDQFy-Kj8RZkEGqXd-Y95O47h8Jszs,171
|
|
15
15
|
schemathesis/cli/commands/run/__init__.py,sha256=_ApiSVh9q-TsJQ_-IiVBNnLCtTCDMTnOLwuJhOvbCp4,18925
|
|
16
16
|
schemathesis/cli/commands/run/context.py,sha256=vej33l5yOhlJ5gLXDwat9WCW_XdhrHNc9pdIQQYddoY,9004
|
|
17
17
|
schemathesis/cli/commands/run/events.py,sha256=ew0TQOc9T2YBZynYWv95k9yfAk8-hGuZDLMxjT8EhvY,1595
|
|
18
18
|
schemathesis/cli/commands/run/executor.py,sha256=_koznTX0DoELPN_1mxr9K_Qg7-9MPXWdld1MFn3YG_Y,5329
|
|
19
19
|
schemathesis/cli/commands/run/filters.py,sha256=pzkNRcf5vLPSsMfnvt711GNzRSBK5iZIFjPA0fiH1N4,1701
|
|
20
|
-
schemathesis/cli/commands/run/loaders.py,sha256=
|
|
20
|
+
schemathesis/cli/commands/run/loaders.py,sha256=eRgP1ZPfhOfxR7iXQ_CfV9r_8jP1DN4tg2aORuZLmOM,4538
|
|
21
21
|
schemathesis/cli/commands/run/validation.py,sha256=DQaMiBLN2tYT9hONvv8xnyPvNXZH768UlOdUxTd5kZs,9193
|
|
22
22
|
schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
|
|
23
23
|
schemathesis/cli/commands/run/handlers/base.py,sha256=qUtDvtr3F6were_BznfnaPpMibGJMnQ5CA9aEzcIUBc,1306
|
|
@@ -28,7 +28,7 @@ schemathesis/cli/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
28
28
|
schemathesis/cli/ext/fs.py,sha256=dHQYBjQozQmuSSfXVp-2KWFK0ESOb_w-lV2SptfMfco,461
|
|
29
29
|
schemathesis/cli/ext/groups.py,sha256=kQ37t6qeArcKaY2y5VxyK3_KwAkBKCVm58IYV8gewds,2720
|
|
30
30
|
schemathesis/cli/ext/options.py,sha256=6yYwZNJL__FCEEL7kI3r5MbVmbp3ZeQjm7DrZ6J_h7s,3347
|
|
31
|
-
schemathesis/config/__init__.py,sha256=
|
|
31
|
+
schemathesis/config/__init__.py,sha256=5jckLTurHbe8uEn656_2rVU-xLAUOUDazUwyECXaT90,6391
|
|
32
32
|
schemathesis/config/_auth.py,sha256=83RLVPm97W2thbn-yi01Rt94YwOxLG_a5VoxhEfjUjs,1528
|
|
33
33
|
schemathesis/config/_checks.py,sha256=F0r16eSSiICvoiTUkNNOE2PH73EGd8bikoeZdME_3Yw,10763
|
|
34
34
|
schemathesis/config/_diff_base.py,sha256=U7wuE4480EjP3K16mfC528TP5q7Q5IwAZwZLqRIrS1E,4300
|
|
@@ -93,25 +93,24 @@ schemathesis/engine/phases/unit/_executor.py,sha256=YDibV3lkC2UMHLvh1FSmnlaQ-SJS
|
|
|
93
93
|
schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
|
|
94
94
|
schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
|
|
95
95
|
schemathesis/generation/case.py,sha256=SLMw6zkzmeiZdaIij8_0tjTF70BrMlRSWREaqWii0uM,12508
|
|
96
|
-
schemathesis/generation/coverage.py,sha256=
|
|
96
|
+
schemathesis/generation/coverage.py,sha256=xQSqeffVlmRGk-MZMcQZwSMhMluO8fS4d9Q7Is-viQ4,60747
|
|
97
97
|
schemathesis/generation/meta.py,sha256=tXhUZBEdpQMn68uMx1SW8Vv59Uf6Wl6yzs-VB9lu_8o,2589
|
|
98
98
|
schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
|
|
99
99
|
schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
|
|
100
100
|
schemathesis/generation/overrides.py,sha256=xI2djHsa42fzP32xpxgxO52INixKagf5DjDAWJYswM8,3890
|
|
101
101
|
schemathesis/generation/hypothesis/__init__.py,sha256=68BHULoXQC1WjFfw03ga5lvDGZ-c-J7H_fNEuUzFWRw,4976
|
|
102
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
|
102
|
+
schemathesis/generation/hypothesis/builder.py,sha256=pnPfJIXBYKyju98xiGUvavh5W2xvuO89RO_NOsvdxYQ,38443
|
|
103
103
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
|
104
104
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
|
105
105
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
|
106
|
-
schemathesis/generation/hypothesis/strategies.py,sha256=RurE81E06d99YKG48dizy9346ayfNswYTt38zewmGgw,483
|
|
107
106
|
schemathesis/generation/stateful/__init__.py,sha256=s7jiJEnguIj44IsRyMi8afs-8yjIUuBbzW58bH5CHjs,1042
|
|
108
|
-
schemathesis/generation/stateful/state_machine.py,sha256=
|
|
107
|
+
schemathesis/generation/stateful/state_machine.py,sha256=3whIW5WDL_-IZIeZLB-qlxIr0_DNC6fb6pZ_0U7ifkE,9285
|
|
109
108
|
schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
|
|
110
109
|
schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
|
|
111
110
|
schemathesis/graphql/loaders.py,sha256=2tgG4HIvFmjHLr_KexVXnT8hSBM-dKG_fuXTZgE97So,9445
|
|
112
111
|
schemathesis/openapi/__init__.py,sha256=-KcsSAM19uOM0N5J4s-yTnQ1BFsptYhW1E51cEf6kVM,311
|
|
113
112
|
schemathesis/openapi/checks.py,sha256=nrkkagRqg-HOsDCAMbJqCnHyBZEA2PpRV_AB8lI_I9c,13080
|
|
114
|
-
schemathesis/openapi/loaders.py,sha256
|
|
113
|
+
schemathesis/openapi/loaders.py,sha256=aaCIf6P8R33l6DBNGD_99m_wruYOPR7ecyL5hT6UChg,10710
|
|
115
114
|
schemathesis/openapi/generation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
116
115
|
schemathesis/openapi/generation/filters.py,sha256=pY9cUZdL_kQK80Z2aylTOqqa12zmaYUlYC5BfYgeQMk,2395
|
|
117
116
|
schemathesis/pytest/__init__.py,sha256=7W0q-Thcw03IAQfXE_Mo8JPZpUdHJzfu85fjK1ZdfQM,88
|
|
@@ -133,7 +132,7 @@ schemathesis/specs/openapi/_hypothesis.py,sha256=O8vN-koBjzBVZfpD3pmgIt6ecU4ddAP
|
|
|
133
132
|
schemathesis/specs/openapi/checks.py,sha256=12ks0V2F8-YKPkItgAc0ZrxsHufWWlsgj-jpj-cF40A,31578
|
|
134
133
|
schemathesis/specs/openapi/converter.py,sha256=4a6-8STT5snF7B-t6IsOIGdK5rV16oNqsdvWL7VFf2M,6472
|
|
135
134
|
schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
|
|
136
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
|
135
|
+
schemathesis/specs/openapi/examples.py,sha256=uHV1HRqFhwpGNsBWHt7WmehyIyr8d-n-VeKKs4FRt2c,24475
|
|
137
136
|
schemathesis/specs/openapi/formats.py,sha256=4tYRdckauHxkJCmOhmdwDq_eOpHPaKloi89lzMPbPzw,3975
|
|
138
137
|
schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKRcUOc41eEwKbo,2472
|
|
139
138
|
schemathesis/specs/openapi/patterns.py,sha256=GqPZEXMRdWENQxanWjBOalIZ2MQUjuxk21kmdiI703E,18027
|
|
@@ -146,7 +145,7 @@ schemathesis/specs/openapi/adapter/parameters.py,sha256=bVo7sgN5oCH2GVDXlqAMrwN0
|
|
|
146
145
|
schemathesis/specs/openapi/adapter/protocol.py,sha256=VDF6COcilHEUnmw76YBVur8bFiTFQHsNvaO9pR_i_KM,2709
|
|
147
146
|
schemathesis/specs/openapi/adapter/references.py,sha256=6M59pJy_U_sLh3Xzgu6-izWXtz3bjXnqJYSD65wRHtk,549
|
|
148
147
|
schemathesis/specs/openapi/adapter/responses.py,sha256=UXcYb048SeS0MhydQY518IgYD0s0Q5YpLsBbdX5-5-s,13276
|
|
149
|
-
schemathesis/specs/openapi/adapter/security.py,sha256=
|
|
148
|
+
schemathesis/specs/openapi/adapter/security.py,sha256=F3K3_LsdUI61O8C-tUhWd3CyBjlwZ_ujhIoUZqzb90A,4945
|
|
150
149
|
schemathesis/specs/openapi/adapter/v2.py,sha256=2Rd1cTv7_I5QrBPLVfa2yD80NAErxV3tdeACjtEfXAA,1280
|
|
151
150
|
schemathesis/specs/openapi/adapter/v3_0.py,sha256=8bOE9WUDrvPivGs0w-S1PP2TXgWuaoTzMdg2_WWbi-E,1272
|
|
152
151
|
schemathesis/specs/openapi/adapter/v3_1.py,sha256=Hi4iMQdLDAeqSVYjafXbRb5yolWuqMz9A954tE2SCQY,1282
|
|
@@ -160,16 +159,16 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=B78vps314fJOMZwlPdv7vUHo7
|
|
|
160
159
|
schemathesis/specs/openapi/negative/mutations.py,sha256=9U352xJsdZBR-Zfy1V7_X3a5i91LIUS9Zqotrzp3BLA,21000
|
|
161
160
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
|
162
161
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
|
163
|
-
schemathesis/specs/openapi/stateful/__init__.py,sha256=
|
|
162
|
+
schemathesis/specs/openapi/stateful/__init__.py,sha256=T-iYOxPh3GfvKUxrc2f2u_GSeO0HUYajn2qVw2F6sGA,18802
|
|
164
163
|
schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
|
|
165
164
|
schemathesis/specs/openapi/stateful/inference.py,sha256=B99jSTDVi2yKxU7-raIb91xpacOrr0nZkEZY5Ej3eCY,9783
|
|
166
|
-
schemathesis/specs/openapi/stateful/links.py,sha256=
|
|
167
|
-
schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=
|
|
168
|
-
schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=
|
|
165
|
+
schemathesis/specs/openapi/stateful/links.py,sha256=P4CISEi-BVRtXd9cXBnHuvxInxW1LBa7DVYcnaZAhBU,8530
|
|
166
|
+
schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=9FWF7tiP7GaOwapRFIYjsu16LxkosKCzBvzjkSTCsjU,8183
|
|
167
|
+
schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=sQydINThS6vp9-OnTKCb_unoVP4m3Ho-0xTG0K7ps8Q,15915
|
|
169
168
|
schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=Kl482Hwq2M8lYAdqGmf_8Yje3voSj1WLDUIujRUDWDQ,12286
|
|
170
169
|
schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=NnXEFY1W3i18jEEYGgC_8oLoE7YOxdXgcMYtZvLj10w,12920
|
|
171
170
|
schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
|
|
172
|
-
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=
|
|
171
|
+
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=MLitJnn1vUihhzuCIA-l7uXG6ne3YTUlnyAAbKaz2Ls,11824
|
|
173
172
|
schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=RaG1BJH4D7-o5Qs2rIRQvS8ERntMUEs2I5jXUFaKMRo,14147
|
|
174
173
|
schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
|
|
175
174
|
schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
|
|
@@ -179,8 +178,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
|
|
|
179
178
|
schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
|
|
180
179
|
schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
|
|
181
180
|
schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
|
|
182
|
-
schemathesis-4.3.
|
|
183
|
-
schemathesis-4.3.
|
|
184
|
-
schemathesis-4.3.
|
|
185
|
-
schemathesis-4.3.
|
|
186
|
-
schemathesis-4.3.
|
|
181
|
+
schemathesis-4.3.12.dist-info/METADATA,sha256=udwQ_n-qA4_1ZwxJDDhMNe6WUrUp4LJ8mnDdbOhapFY,8566
|
|
182
|
+
schemathesis-4.3.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
183
|
+
schemathesis-4.3.12.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
|
184
|
+
schemathesis-4.3.12.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
|
185
|
+
schemathesis-4.3.12.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from functools import reduce
|
|
4
|
-
from operator import or_
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from hypothesis import strategies as st
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def combine(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
|
|
12
|
-
"""Combine a list of strategies into a single one.
|
|
13
|
-
|
|
14
|
-
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
|
15
|
-
"""
|
|
16
|
-
return reduce(or_, strategies[1:], strategies[0])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|