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.

@@ -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
@@ -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 combine([STRATEGIES_FOR_TYPE[t] for t in ty if t in STRATEGIES_FOR_TYPE])
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, strategies
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 = strategies.combine(
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
- # Whether this transition was actually applied
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
- is_applied: bool
43
+ applied_parameters: list[str]
44
44
 
45
- __slots__ = ("case", "transition", "is_applied")
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, is_applied=False)
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
@@ -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
- kind = LoaderErrorKind.SYNTAX_ERROR
307
- message = SCHEMA_SYNTAX_ERROR
308
- extras = [entry for entry in str(exc).splitlines() if entry]
309
- raise LoaderError(kind, message, extras=extras) from exc
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 strategies.combine(_strategies)
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 strategies.combine(_strategies)
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-securuty-type"
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
- # Try to remove recursive references to avoid infinite recursion
220
- remaining_references = references.sanitize(schema)
221
- if reference in remaining_references:
222
- cycle = list(reference_path[reference_path.index(reference) :])
223
- raise InfiniteRecursiveReference(reference, cycle)
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
- schema, current_path = _resolve_bundled(schema, resolver, reference_path)
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
- subschema, _ = _resolve_bundled(subschema, resolver, current_path)
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
- sub, _ = _resolve_bundled(sub, resolver, current_path)
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, current_path)
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
- schema, current_path = _resolve_bundled(schema, resolver, reference_path)
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
- USE_TRANSITION_THRESHOLD = 85
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
- kwargs: dict[str, Any] = {
261
- container: {
262
- name: extracted.value.ok()
263
- for name, extracted in data.items()
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
- if (
270
- transition.request_body is not None
271
- and isinstance(transition.request_body.value, Ok)
272
- and transition.request_body.value.ok() is not UNRESOLVABLE
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
- is_applied = bool(kwargs)
277
+ param_key = f"{container}.{name}"
279
278
 
280
- cases = strategies.combine(
281
- [target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **kwargs) for mode in modes]
282
- )
283
- case = draw(cases)
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
- new = transition.request_body.value.ok()
292
- if isinstance(case.body, dict) and isinstance(new, dict):
293
- case.body = {**case.body, **new}
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
- case.body = new
296
- is_applied = True
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, is_applied=is_applied)
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 extract_inputs, update_input_field_bindings
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.path_parameters:
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(definition=parameter.expression, value=value)
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.10
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=jq2KSphzfLn63FNROrc3GwukTDt4ih4-RmF0Z4WSpu8,27582
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=DNzKEnXu7GjGSVe0244ZErmygUBA3nGSyVY6JP3ixD0,3740
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=6j0ez7wduAUYbUT28ELKxMf-dYEWr_67m_KIuTSyNGk,4358
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=fXE_NxvAxhyGag0_gC3xJwUrUkxZz4OKANQ1nYuGFQk,6311
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=uUUND4zF17hqs9L1VtNz41KtxcW-BB9JDsrNBA-w2-w,60813
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=wOCjNAScpiN-wJ4EM0QnJ_5o9nczW0WILFNfIR1qeKQ,38480
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=25kkYImw5byNwuTtt97aNE3kTHAF8rZ-p3ax_bmd3JI,9135
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=-DSFWcvD_PmekTyy0qZrJ1YYODh9C1KuAJJIytEnS1s,10733
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=moFFfOfzepjlJOrqLc60BrEmJ4oRzwJ3SM03y_nJNMU,24097
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=W3cqlbs80NxF9SAavOi7BhtNGzdxHO476lYxiWN0D08,4945
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=CQx2WJ3mKn5qmYRc90DqsG9w3Gx7DrB60S9HFz81STY,16663
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=SSA66mU50FFBz7e6sA37CfL-Vt0OY3gont72oFSvZYU,8163
167
- schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=0JM-FrY6Awv6gl-qDHaaK7pXbt_GKutBKPyIaph8apA,7842
168
- schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=PSactImp4OqsYMHUl2gB2pgvUlZCCKJRJKeaalclFzU,11511
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=p58XoADpMKFAun0Bx_rul-kiUlfA9PXjxHJ97dT2tBE,11202
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.10.dist-info/METADATA,sha256=BrfLLUJxe-ve_0xCRM77F5ksA27XYwnQaS_tf7pawQM,8541
183
- schemathesis-4.3.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
- schemathesis-4.3.10.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
- schemathesis-4.3.10.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
- schemathesis-4.3.10.dist-info/RECORD,,
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])