schemathesis 4.3.11__py3-none-any.whl → 4.3.13__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.

@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import enum
6
6
  import re
7
7
  import traceback
8
+ from dataclasses import dataclass
9
+ from textwrap import indent
8
10
  from types import TracebackType
9
11
  from typing import TYPE_CHECKING, Any, Callable, NoReturn
10
12
 
@@ -33,6 +35,60 @@ class SchemathesisError(Exception):
33
35
  """Base exception class for all Schemathesis errors."""
34
36
 
35
37
 
38
+ class DefinitionKind(str, enum.Enum):
39
+ SCHEMA = "Schema Object"
40
+ SECURITY_SCHEME = "Security Scheme Object"
41
+ RESPONSES = "Responses Object"
42
+ PARAMETER = "Parameter Object"
43
+
44
+
45
+ @dataclass
46
+ class SchemaLocation:
47
+ kind: DefinitionKind
48
+ # Hint about where the definition is located
49
+ hint: str | None
50
+ # Open API spec version
51
+ version: str
52
+
53
+ __slots__ = ("kind", "hint", "version")
54
+
55
+ @classmethod
56
+ def response_schema(cls, version: str) -> SchemaLocation:
57
+ return cls(kind=DefinitionKind.SCHEMA, hint="in response definition", version=version)
58
+
59
+ @classmethod
60
+ def maybe_from_error_path(cls, path: list[str | int], version: str) -> SchemaLocation | None:
61
+ if len(path) == 3 and path[:2] == ["components", "securitySchemes"]:
62
+ return cls(kind=DefinitionKind.SECURITY_SCHEME, hint=f"definition for `{path[2]}`", version=version)
63
+ if len(path) == 3 and path[:2] == ["components", "schemas"]:
64
+ return cls(kind=DefinitionKind.SCHEMA, hint=f"definition for `{path[2]}`", version=version)
65
+ if len(path) == 4 and path[0] == "paths" and path[-1] == "responses":
66
+ return cls(kind=DefinitionKind.RESPONSES, hint=None, version=version)
67
+ if len(path) == 5 and path[0] == "paths" and path[3] == "parameters":
68
+ return cls(kind=DefinitionKind.PARAMETER, hint=f"at index {path[4]}", version=version)
69
+
70
+ return None
71
+
72
+ @property
73
+ def message(self) -> str:
74
+ message = f"Invalid {self.kind.value}"
75
+ if self.hint is not None:
76
+ message += f" {self.hint}"
77
+ else:
78
+ message += " definition"
79
+ return message
80
+
81
+ @property
82
+ def specification_url(self) -> str:
83
+ anchor = {
84
+ DefinitionKind.SCHEMA: "schema-object",
85
+ DefinitionKind.SECURITY_SCHEME: "security-scheme-object",
86
+ DefinitionKind.RESPONSES: "responses-object",
87
+ DefinitionKind.PARAMETER: "parameter-object",
88
+ }[self.kind]
89
+ return f"https://spec.openapis.org/oas/v{self.version}#{anchor}"
90
+
91
+
36
92
  class InvalidSchema(SchemathesisError):
37
93
  """Indicates errors in API schema validation or processing."""
38
94
 
@@ -56,9 +112,16 @@ class InvalidSchema(SchemathesisError):
56
112
 
57
113
  @classmethod
58
114
  def from_jsonschema_error(
59
- cls, error: ValidationError | JsonSchemaError, path: str | None, method: str | None, config: OutputConfig
115
+ cls,
116
+ error: ValidationError | JsonSchemaError,
117
+ path: str | None,
118
+ method: str | None,
119
+ config: OutputConfig,
120
+ location: SchemaLocation | None = None,
60
121
  ) -> InvalidSchema:
61
- if error.absolute_path:
122
+ if location is not None:
123
+ message = location.message
124
+ elif error.absolute_path:
62
125
  part = error.absolute_path[-1]
63
126
  if isinstance(part, int) and len(error.absolute_path) > 1:
64
127
  parent = error.absolute_path[-2]
@@ -70,14 +133,18 @@ class InvalidSchema(SchemathesisError):
70
133
  error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
71
134
  message += f"\n\nLocation:\n {error_path}"
72
135
  instance = truncate_json(error.instance, config=config)
73
- message += f"\n\nProblematic definition:\n{instance}"
136
+ message += f"\n\nProblematic definition:\n{indent(instance, ' ')}"
74
137
  message += "\n\nError details:\n "
75
138
  # This default message contains the instance which we already printed
76
139
  if "is not valid under any of the given schemas" in error.message:
77
140
  message += "The provided definition doesn't match any of the expected formats or types."
78
141
  else:
79
142
  message += error.message
80
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
143
+ message += "\n\n"
144
+ if location is not None:
145
+ message += f"See: {location.specification_url}"
146
+ else:
147
+ message += SCHEMA_ERROR_SUGGESTION
81
148
  return cls(message, path=path, method=method)
82
149
 
83
150
  @classmethod
@@ -86,12 +153,14 @@ class InvalidSchema(SchemathesisError):
86
153
  ) -> InvalidSchema:
87
154
  notes = getattr(error, "__notes__", [])
88
155
  # Some exceptions don't have the actual reference in them, hence we add it manually via notes
89
- pointer = f"'{notes[0]}'"
90
- message = "Unresolvable JSON pointer in the schema"
156
+ reference = str(notes[0])
157
+ message = "Unresolvable reference in the schema"
91
158
  # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
92
- message += f"\n\nError details:\n JSON pointer: {pointer}"
93
- message += "\n This typically means that the schema is referencing a component that doesn't exist."
94
- message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
159
+ message += f"\n\nError details:\n Reference: {reference}"
160
+ if not reference.startswith(("http://", "https://", "#/")):
161
+ message += "\n File reference could not be resolved. Check that the file exists."
162
+ elif reference.startswith(("#/components", "#/definitions")):
163
+ message += "\n Component does not exist in the schema."
95
164
  return cls(message, path=path, method=method)
96
165
 
97
166
  def as_failing_test_function(self) -> Callable:
@@ -154,13 +223,13 @@ class InvalidStateMachine(SchemathesisError):
154
223
  for source, target_groups in by_source.items():
155
224
  for (target, status), transitions in target_groups.items():
156
225
  for transition in transitions:
157
- result += f"\n\n {_format_transition(source, status, transition.name, target)}\n"
226
+ result += f"\n\n {format_transition(source, status, transition.name, target)}\n"
158
227
  for error in transition.errors:
159
228
  result += f"\n - {error.message}"
160
229
  return result
161
230
 
162
231
 
163
- def _format_transition(source: str, status: str, transition: str, target: str) -> str:
232
+ def format_transition(source: str, status: str, transition: str, target: str) -> str:
164
233
  return f"{source} -> [{status}] {transition} -> {target}"
165
234
 
166
235
 
@@ -25,6 +25,7 @@ from schemathesis.core.errors import (
25
25
  InvalidRegexType,
26
26
  InvalidSchema,
27
27
  MalformedMediaType,
28
+ SchemaLocation,
28
29
  SerializationNotPossible,
29
30
  )
30
31
  from schemathesis.core.failures import Failure, FailureGroup
@@ -198,6 +199,9 @@ def run_test(
198
199
  path=operation.path,
199
200
  method=operation.method,
200
201
  config=ctx.config.output,
202
+ location=SchemaLocation.maybe_from_error_path(
203
+ list(exc.absolute_path), ctx.schema.specification.version
204
+ ),
201
205
  )
202
206
  )
203
207
  except InvalidArgument as exc:
@@ -37,16 +37,23 @@ 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
+ # If the transition has no parameters or body, count it as applied
54
+ if self.transition is not None and not self.transition.parameters and self.transition.request_body is None:
55
+ return True
56
+ return bool(self.applied_parameters)
50
57
 
51
58
 
52
59
  @dataclass
@@ -69,8 +76,9 @@ class ExtractedParam:
69
76
 
70
77
  definition: Any
71
78
  value: Result[Any, Exception]
79
+ is_required: bool
72
80
 
73
- __slots__ = ("definition", "value")
81
+ __slots__ = ("definition", "value", "is_required")
74
82
 
75
83
 
76
84
  @dataclass
@@ -27,7 +27,13 @@ from requests.structures import CaseInsensitiveDict
27
27
  from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, Specification, deserialization, media_types
28
28
  from schemathesis.core.adapter import OperationParameter, ResponsesContainer
29
29
  from schemathesis.core.compat import RefResolutionError
30
- from schemathesis.core.errors import InfiniteRecursiveReference, InvalidSchema, OperationNotFound
30
+ from schemathesis.core.errors import (
31
+ SCHEMA_ERROR_SUGGESTION,
32
+ InfiniteRecursiveReference,
33
+ InvalidSchema,
34
+ OperationNotFound,
35
+ SchemaLocation,
36
+ )
31
37
  from schemathesis.core.failures import Failure, FailureGroup, MalformedJson
32
38
  from schemathesis.core.result import Err, Ok, Result
33
39
  from schemathesis.core.transport import Response
@@ -61,7 +67,6 @@ if TYPE_CHECKING:
61
67
  from schemathesis.generation.stateful import APIStateMachine
62
68
 
63
69
  HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
64
- SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
65
70
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, RefResolutionError, InvalidSchema, InfiniteRecursiveReference)
66
71
 
67
72
 
@@ -317,9 +322,13 @@ class BaseOpenAPISchema(BaseSchema):
317
322
  self.validate()
318
323
  except jsonschema.ValidationError as exc:
319
324
  raise InvalidSchema.from_jsonschema_error(
320
- exc, path=path, method=method, config=self.config.output
325
+ exc,
326
+ path=path,
327
+ method=method,
328
+ config=self.config.output,
329
+ location=SchemaLocation.maybe_from_error_path(list(exc.absolute_path), self.specification.version),
321
330
  ) from None
322
- raise InvalidSchema(SCHEMA_ERROR_MESSAGE, path=path, method=method) from error
331
+ raise InvalidSchema(SCHEMA_ERROR_SUGGESTION, path=path, method=method) from error
323
332
 
324
333
  def validate(self) -> None:
325
334
  with suppress(TypeError):
@@ -540,7 +549,11 @@ class BaseOpenAPISchema(BaseSchema):
540
549
  definition.validator.validate(data)
541
550
  except jsonschema.SchemaError as exc:
542
551
  raise InvalidSchema.from_jsonschema_error(
543
- exc, path=operation.path, method=operation.method, config=self.config.output
552
+ exc,
553
+ path=operation.path,
554
+ method=operation.method,
555
+ config=self.config.output,
556
+ location=SchemaLocation.response_schema(self.specification.version),
544
557
  ) from exc
545
558
  except jsonschema.ValidationError as exc:
546
559
  failures.append(
@@ -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
@@ -49,7 +50,7 @@ class OpenAPIStateMachine(APIStateMachine):
49
50
  # The proportion of negative tests generated for "root" transitions
50
51
  NEGATIVE_TEST_CASES_THRESHOLD = 10
51
52
  # How often some transition is skipped
52
- USE_TRANSITION_THRESHOLD = 85
53
+ BASE_EXPLORATION_RATE = 0.15
53
54
 
54
55
 
55
56
  @dataclass
@@ -162,14 +163,12 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
162
163
  for target in operations:
163
164
  if target.label in transitions.operations:
164
165
  incoming = transitions.operations[target.label].incoming
166
+ config = schema.config.generation_for(operation=target, phase="stateful")
165
167
  if incoming:
166
168
  for link in incoming:
167
169
  bundle_name = f"{link.source.label} -> {link.status_code}"
168
- name = _normalize_name(
169
- f"{link.source.label} -> {link.status_code} -> {link.name} -> {target.label}"
170
- )
170
+ name = _normalize_name(link.full_name)
171
171
  assert name not in rules, name
172
- config = schema.config.generation_for(operation=target, phase="stateful")
173
172
  rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
174
173
  transition(
175
174
  name=name,
@@ -181,7 +180,6 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
181
180
  )
182
181
  if target.label in roots.reliable or (not roots.reliable and target.label in roots.fallback):
183
182
  name = _normalize_name(f"RANDOM -> {target.label}")
184
- config = schema.config.generation_for(operation=target, phase="stateful")
185
183
  if len(config.modes) == 1:
186
184
  case_strategy = target.as_strategy(generation_mode=config.modes[0], phase=TestPhase.STATEFUL)
187
185
  else:
@@ -237,8 +235,8 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
237
235
 
238
236
  def is_likely_root_transition(operation: APIOperation) -> bool:
239
237
  """Check if operation is likely to succeed as a root transition."""
240
- # POST operations with request bodies are likely to create resources
241
- if operation.method == "post" and operation.body:
238
+ # POST operations are likely to create resources
239
+ if operation.method == "post":
242
240
  return True
243
241
 
244
242
  # GET operations without path parameters are likely to return lists
@@ -249,50 +247,97 @@ def is_likely_root_transition(operation: APIOperation) -> bool:
249
247
 
250
248
 
251
249
  def into_step_input(
252
- target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
250
+ *, target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
253
251
  ) -> Callable[[StepOutput], st.SearchStrategy[StepInput]]:
252
+ """A single transition between API operations."""
253
+
254
254
  def builder(_output: StepOutput) -> st.SearchStrategy[StepInput]:
255
255
  @st.composite # type: ignore[misc]
256
256
  def inner(draw: st.DrawFn, output: StepOutput) -> StepInput:
257
+ random = draw(st.randoms(use_true_random=True))
258
+
259
+ def biased_coin(p: float) -> bool:
260
+ return random.random() < p
261
+
262
+ # Extract transition data from previous operation's output
257
263
  transition = link.extract(output)
258
264
 
259
- kwargs: dict[str, Any] = {
260
- container: {
261
- name: extracted.value.ok()
262
- for name, extracted in data.items()
263
- if isinstance(extracted.value, Ok) and extracted.value.ok() not in (None, UNRESOLVABLE)
264
- }
265
- for container, data in transition.parameters.items()
266
- }
265
+ overrides: dict[str, Any] = {}
266
+ applied_parameters = []
267
+ for container, data in transition.parameters.items():
268
+ overrides[container] = {}
269
+
270
+ for name, extracted in data.items():
271
+ # Skip if extraction failed or returned unusable value
272
+ if not isinstance(extracted.value, Ok) or extracted.value.ok() in (None, UNRESOLVABLE):
273
+ continue
274
+
275
+ param_key = f"{container}.{name}"
276
+
277
+ # Calculate exploration rate based on parameter characteristics
278
+ exploration_rate = BASE_EXPLORATION_RATE
279
+
280
+ # Path parameters are critical for routing - use link values more often
281
+ if container == "path_parameters":
282
+ exploration_rate *= 0.5
283
+
284
+ # Required parameters should follow links more often, optional ones explored more
285
+ # Path params are always required, so they get both multipliers
286
+ if extracted.is_required:
287
+ exploration_rate *= 0.5
288
+ else:
289
+ # Explore optional parameters more to avoid only testing link-provided values
290
+ exploration_rate *= 3.0
267
291
 
292
+ if biased_coin(1 - exploration_rate):
293
+ overrides[container][name] = extracted.value.ok()
294
+ applied_parameters.append(param_key)
295
+
296
+ # Get the extracted body value
268
297
  if (
269
298
  transition.request_body is not None
270
299
  and isinstance(transition.request_body.value, Ok)
271
300
  and transition.request_body.value.ok() is not UNRESOLVABLE
272
- and not link.merge_body
273
- and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
274
301
  ):
275
- kwargs["body"] = transition.request_body.value.ok()
276
-
277
- is_applied = bool(kwargs)
302
+ request_body = transition.request_body.value.ok()
303
+ else:
304
+ request_body = NOT_SET
305
+
306
+ # Link suppose to replace the entire extracted body
307
+ if request_body is not NOT_SET and not link.merge_body and biased_coin(1 - BASE_EXPLORATION_RATE):
308
+ overrides["body"] = request_body
309
+ if isinstance(overrides["body"], dict):
310
+ applied_parameters.extend(f"body.{field}" for field in overrides["body"])
311
+ else:
312
+ applied_parameters.append("body")
278
313
 
279
314
  cases = st.one_of(
280
- target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **kwargs) for mode in modes
315
+ [target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **overrides) for mode in modes]
281
316
  )
282
317
  case = draw(cases)
283
- if (
284
- transition.request_body is not None
285
- and isinstance(transition.request_body.value, Ok)
286
- and transition.request_body.value.ok() is not UNRESOLVABLE
287
- and link.merge_body
288
- and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
289
- ):
290
- new = transition.request_body.value.ok()
291
- if isinstance(case.body, dict) and isinstance(new, dict):
292
- case.body = {**case.body, **new}
293
- else:
294
- case.body = new
295
- is_applied = True
318
+ if request_body is not NOT_SET and link.merge_body:
319
+ if isinstance(request_body, dict):
320
+ selected_fields = {}
321
+
322
+ for field_name, field_value in request_body.items():
323
+ if field_value is UNRESOLVABLE:
324
+ continue
325
+
326
+ if biased_coin(1 - BASE_EXPLORATION_RATE):
327
+ selected_fields[field_name] = field_value
328
+ applied_parameters.append(f"body.{field_name}")
329
+
330
+ if selected_fields:
331
+ if isinstance(case.body, dict):
332
+ case.body = {**case.body, **selected_fields}
333
+ else:
334
+ # Can't merge into non-dict, replace entirely
335
+ case.body = selected_fields
336
+ elif biased_coin(1 - BASE_EXPLORATION_RATE):
337
+ case.body = request_body
338
+ applied_parameters.append("body")
339
+
340
+ # Re-validate generation mode after merging body
296
341
  if case.meta and case.meta.generation.mode == GenerationMode.NEGATIVE:
297
342
  # It is possible that the new body is now valid and the whole test case could be valid too
298
343
  for alternative in case.operation.body:
@@ -304,7 +349,7 @@ def into_step_input(
304
349
  )
305
350
  if all(info.mode == GenerationMode.POSITIVE for info in case.meta.components.values()):
306
351
  case.meta.generation.mode = GenerationMode.POSITIVE
307
- return StepInput(case=case, transition=transition, is_applied=is_applied)
352
+ return StepInput(case=case, transition=transition, applied_parameters=applied_parameters)
308
353
 
309
354
  return inner(output=_output)
310
355
 
@@ -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
+ )
@@ -394,13 +394,13 @@ def find_matching_field(*, parameter: str, resource: str, fields: list[str]) ->
394
394
  return field
395
395
 
396
396
  # Extract parameter components
397
- parameter_prefix, param_suffix = _split_parameter_name(parameter)
397
+ parameter_prefix, parameter_suffix = _split_parameter_name(parameter)
398
398
  parameter_prefix_normalized = _normalize_for_matching(parameter_prefix)
399
399
 
400
400
  # Parameter has resource prefix, field might not
401
401
  # Example: `channelId` - `Channel.id`
402
402
  if parameter_prefix and parameter_prefix_normalized == resource_normalized:
403
- suffix_normalized = _normalize_for_matching(param_suffix)
403
+ suffix_normalized = _normalize_for_matching(parameter_suffix)
404
404
 
405
405
  for field in fields:
406
406
  field_normalized = _normalize_for_matching(field)
@@ -409,8 +409,8 @@ def find_matching_field(*, parameter: str, resource: str, fields: list[str]) ->
409
409
 
410
410
  # Parameter has no prefix, field might have resource prefix
411
411
  # Example: `id` - `Channel.channelId`
412
- if not parameter_prefix and param_suffix:
413
- expected_field_normalized = resource_normalized + _normalize_for_matching(param_suffix)
412
+ if not parameter_prefix and parameter_suffix:
413
+ expected_field_normalized = resource_normalized + _normalize_for_matching(parameter_suffix)
414
414
 
415
415
  for field in fields:
416
416
  field_normalized = _normalize_for_matching(field)
@@ -433,7 +433,7 @@ def _normalize_for_matching(text: str) -> str:
433
433
  return text.lower().replace("_", "").replace("-", "")
434
434
 
435
435
 
436
- def _split_parameter_name(param_name: str) -> tuple[str, str]:
436
+ def _split_parameter_name(parameter_name: str) -> tuple[str, str]:
437
437
  """Split parameter into (prefix, suffix) components.
438
438
 
439
439
  Examples:
@@ -444,13 +444,16 @@ def _split_parameter_name(param_name: str) -> tuple[str, str]:
444
444
  "channel_id" -> ("channel", "_id")
445
445
 
446
446
  """
447
- if param_name.endswith("Id") and len(param_name) > 2:
448
- return (param_name[:-2], "Id")
447
+ if parameter_name.endswith("Id") and len(parameter_name) > 2:
448
+ return (parameter_name[:-2], "Id")
449
449
 
450
- if param_name.endswith("_id") and len(param_name) > 3:
451
- return (param_name[:-3], "_id")
450
+ if parameter_name.endswith("_id") and len(parameter_name) > 3:
451
+ return (parameter_name[:-3], "_id")
452
452
 
453
- return ("", param_name)
453
+ if parameter_name.endswith("_guid") and len(parameter_name) > 5:
454
+ return (parameter_name[:-5], "_guid")
455
+
456
+ return ("", parameter_name)
454
457
 
455
458
 
456
459
  def strip_affixes(name: str, prefixes: list[str], suffixes: list[str]) -> str:
@@ -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,
@@ -293,7 +294,13 @@ def _extract_resource_from_schema(
293
294
  properties = resolved.get("properties")
294
295
  if properties:
295
296
  fields = sorted(properties)
296
- types = {field: set(get_type(subschema)) for field, subschema in properties.items()}
297
+ types = {}
298
+ for field, subschema in properties.items():
299
+ if isinstance(subschema, dict):
300
+ _, resolved_subschema = maybe_resolve(subschema, resolver, "")
301
+ else:
302
+ resolved_subschema = subschema
303
+ types[field] = set(get_type(cast(dict, resolved_subschema)))
297
304
  source = DefinitionSource.SCHEMA_WITH_PROPERTIES
298
305
  else:
299
306
  fields = []
@@ -310,3 +317,18 @@ def _extract_resource_from_schema(
310
317
  resources[resource_name] = resource
311
318
 
312
319
  return resource
320
+
321
+
322
+ def remove_unused_resources(operations: OperationMap, resources: ResourceMap) -> None:
323
+ """Remove resources that aren't referenced by any operation."""
324
+ # Collect all resource names currently in use
325
+ used_resources = set()
326
+ for operation in operations.values():
327
+ for input_slot in operation.inputs:
328
+ used_resources.add(input_slot.resource.name)
329
+ for output_slot in operation.outputs:
330
+ used_resources.add(output_slot.resource.name)
331
+
332
+ unused = set(resources.keys()) - used_resources
333
+ for resource_name in unused:
334
+ del resources[resource_name]
@@ -232,30 +232,38 @@ def unwrap_schema(
232
232
  _, resolved = maybe_resolve(array_schema, resolver, "")
233
233
  pointer = f"/{encode_pointer(array_field)}"
234
234
 
235
+ uses_parent_ref = False
235
236
  # Try to unwrap one more time
236
237
  if resolved.get("type") == "array" or "items" in resolved:
237
238
  nested_items = resolved.get("items")
238
239
  if isinstance(nested_items, dict):
239
240
  _, resolved_items = maybe_resolve(nested_items, resolver, "")
240
- external_tag = _detect_externally_tagged_pattern(resolved_items, path)
241
+ external_tag = _detect_externally_tagged_pattern(resolved_items, path, parent_ref)
241
242
  if external_tag:
242
- nested_properties = resolved_items["properties"][external_tag]
243
+ external_tag_, uses_parent_ref = external_tag
244
+ nested_properties = resolved_items["properties"][external_tag_]
243
245
  _, resolved = maybe_resolve(nested_properties, resolver, "")
244
- pointer += f"/{encode_pointer(external_tag)}"
246
+ pointer += f"/{encode_pointer(external_tag_)}"
245
247
 
248
+ ref = parent_ref if uses_parent_ref else array_schema.get("$ref")
246
249
  return UnwrappedSchema(pointer=pointer, schema=resolved, ref=array_schema.get("$ref"))
247
250
 
248
251
  # External tag
249
- external_tag = _detect_externally_tagged_pattern(schema, path)
252
+ external_tag = _detect_externally_tagged_pattern(schema, path, parent_ref)
250
253
  if external_tag:
251
- tagged_schema = properties[external_tag]
254
+ external_tag_, uses_parent_ref = external_tag
255
+ tagged_schema = properties[external_tag_]
252
256
  _, resolved_tagged = maybe_resolve(tagged_schema, resolver, "")
253
257
 
254
258
  resolved = try_unwrap_all_of(resolved_tagged)
255
- ref = resolved.get("$ref") or resolved_tagged.get("$ref") or tagged_schema.get("$ref")
259
+ ref = (
260
+ parent_ref
261
+ if uses_parent_ref
262
+ else resolved.get("$ref") or resolved_tagged.get("$ref") or tagged_schema.get("$ref")
263
+ )
256
264
 
257
265
  _, resolved = maybe_resolve(resolved, resolver, "")
258
- return UnwrappedSchema(pointer=f"/{encode_pointer(external_tag)}", schema=resolved, ref=ref)
266
+ return UnwrappedSchema(pointer=f"/{encode_pointer(external_tag_)}", schema=resolved, ref=ref)
259
267
 
260
268
  # No wrapper - single object at root
261
269
  return UnwrappedSchema(pointer="/", schema=schema, ref=schema.get("$ref"))
@@ -391,7 +399,9 @@ def _is_pagination_wrapper(
391
399
  return None
392
400
 
393
401
 
394
- def _detect_externally_tagged_pattern(schema: Mapping[str, Any], path: str) -> str | None:
402
+ def _detect_externally_tagged_pattern(
403
+ schema: Mapping[str, Any], path: str, parent_ref: str | None
404
+ ) -> tuple[str, bool] | None:
395
405
  """Detect externally tagged resource pattern.
396
406
 
397
407
  Pattern: {ResourceName: [...]} or {resourceName: [...]}
@@ -420,12 +430,18 @@ def _detect_externally_tagged_pattern(schema: Mapping[str, Any], path: str) -> s
420
430
  # `data_request`
421
431
  naming.to_snake_case(resource_name),
422
432
  }
433
+ parent_names = set()
434
+ if parent_ref is not None:
435
+ maybe_resource_name = resource_name_from_ref(parent_ref)
436
+ parent_names.add(naming.to_plural(maybe_resource_name.lower()))
437
+ parent_names.add(naming.to_snake_case(maybe_resource_name))
438
+ possible_names = possible_names.union(parent_names)
423
439
 
424
440
  for name, subschema in properties.items():
425
441
  if name.lower() not in possible_names:
426
442
  continue
427
443
 
428
444
  if isinstance(subschema, dict) and "object" in get_type(subschema):
429
- return name
445
+ return name, name.lower() in parent_names
430
446
 
431
447
  return None
@@ -5,7 +5,7 @@ from functools import lru_cache
5
5
  from typing import Any, Callable
6
6
 
7
7
  from schemathesis.core import NOT_SET, NotSet
8
- from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError
8
+ from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError, format_transition
9
9
  from schemathesis.core.parameters import ParameterLocation
10
10
  from schemathesis.core.result import Err, Ok, Result
11
11
  from schemathesis.generation.stateful.state_machine import ExtractedParam, StepOutput, Transition
@@ -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)
@@ -93,6 +94,10 @@ class OpenApiLink:
93
94
 
94
95
  self._cached_extract = lru_cache(8)(self._extract_impl)
95
96
 
97
+ @property
98
+ def full_name(self) -> str:
99
+ return format_transition(self.source.label, self.status_code, self.name, self.target.label)
100
+
96
101
  def _normalize_parameters(
97
102
  self, parameters: dict[str, str], errors: list[TransitionValidationError]
98
103
  ) -> list[NormalizedParameter]:
@@ -131,15 +136,21 @@ class OpenApiLink:
131
136
  except Exception as exc:
132
137
  errors.append(TransitionValidationError(str(exc)))
133
138
 
139
+ is_required = False
134
140
  if hasattr(self, "target"):
135
141
  try:
136
142
  container_name = self._get_parameter_container(location, name)
137
143
  except TransitionValidationError as exc:
138
144
  errors.append(exc)
139
145
  continue
146
+
147
+ for param in self.target.iter_parameters():
148
+ if param.name == name:
149
+ is_required = param.is_required
150
+ break
140
151
  else:
141
152
  continue
142
- result.append(NormalizedParameter(location, name, expression, container_name))
153
+ result.append(NormalizedParameter(location, name, expression, container_name, is_required=is_required))
143
154
  return result
144
155
 
145
156
  def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
@@ -158,7 +169,7 @@ class OpenApiLink:
158
169
  def _extract_impl(self, wrapper: StepOutputWrapper) -> Transition:
159
170
  output = wrapper.output
160
171
  return Transition(
161
- id=f"{self.source.label} -> [{self.status_code}] {self.name} -> {self.target.label}",
172
+ id=self.full_name,
162
173
  parent_id=output.case.id,
163
174
  is_inferred=self.is_inferred,
164
175
  parameters=self.extract_parameters(output),
@@ -178,7 +189,9 @@ class OpenApiLink:
178
189
  value = Ok(expressions.evaluate(parameter.expression, output))
179
190
  except Exception as exc:
180
191
  value = Err(exc)
181
- container[parameter.name] = ExtractedParam(definition=parameter.expression, value=value)
192
+ container[parameter.name] = ExtractedParam(
193
+ definition=parameter.expression, value=value, is_required=parameter.is_required
194
+ )
182
195
  return extracted
183
196
 
184
197
  def extract_body(self, output: StepOutput) -> ExtractedParam | None:
@@ -188,7 +201,7 @@ class OpenApiLink:
188
201
  value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
189
202
  except Exception as exc:
190
203
  value = Err(exc)
191
- return ExtractedParam(definition=self.body, value=value)
204
+ return ExtractedParam(definition=self.body, value=value, is_required=True)
192
205
  return None
193
206
 
194
207
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.3.11
3
+ Version: 4.3.13
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
@@ -52,7 +52,7 @@ schemathesis/core/compat.py,sha256=9BWCrFoqN2sJIaiht_anxe8kLjYMR7t0iiOkXqLRUZ8,1
52
52
  schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,130
53
53
  schemathesis/core/curl.py,sha256=jrPL9KpNHteyJ6A1oxJRSkL5bfuBeuPs3xh9Z_ml2cE,1892
54
54
  schemathesis/core/deserialization.py,sha256=qjXUPaz_mc1OSgXzTUSkC8tuVR8wgVQtb9g3CcAF6D0,2951
55
- schemathesis/core/errors.py,sha256=KlcDlsxg9sHKdo-NtFDluhk1iwQbSHdpDGykf8CxOKY,17357
55
+ schemathesis/core/errors.py,sha256=sr23WgbD-52n5fmC-QBn2suzNUbsB1okvXIs_L5EyR0,19918
56
56
  schemathesis/core/failures.py,sha256=yFpAxWdEnm0Ri8z8RqRI9H7vcLH5ztOeSIi4m4SGx5g,8996
57
57
  schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
58
58
  schemathesis/core/hooks.py,sha256=qhbkkRSf8URJ4LKv2wmKRINKpquUOgxQzWBHKWRWo3Q,475
@@ -89,7 +89,7 @@ schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuU
89
89
  schemathesis/engine/phases/stateful/_executor.py,sha256=yRpUJqKLTKMVRy7hEXPwmI23CtgGIprz341lCJwvTrU,15613
90
90
  schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
91
91
  schemathesis/engine/phases/unit/__init__.py,sha256=9dDcxyj887pktnE9YDIPNaR-vc7iqKQWIrFr77SbUTQ,8786
92
- schemathesis/engine/phases/unit/_executor.py,sha256=YDibV3lkC2UMHLvh1FSmnlaQ-SJS-R0MU2qEF4NBbf0,17235
92
+ schemathesis/engine/phases/unit/_executor.py,sha256=4wr7POpPfeI7_Mx6i2pk2efyK1FxKGjXdMwi_MURTDU,17427
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
@@ -104,7 +104,7 @@ schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8E
104
104
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
105
105
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
106
106
  schemathesis/generation/stateful/__init__.py,sha256=s7jiJEnguIj44IsRyMi8afs-8yjIUuBbzW58bH5CHjs,1042
107
- schemathesis/generation/stateful/state_machine.py,sha256=25kkYImw5byNwuTtt97aNE3kTHAF8rZ-p3ax_bmd3JI,9135
107
+ schemathesis/generation/stateful/state_machine.py,sha256=CiVtpBEeotpNOUkYO3vJLKRe89gdT1kjguZ88vbfqs0,9500
108
108
  schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
109
109
  schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
110
110
  schemathesis/graphql/loaders.py,sha256=2tgG4HIvFmjHLr_KexVXnT8hSBM-dKG_fuXTZgE97So,9445
@@ -137,7 +137,7 @@ schemathesis/specs/openapi/formats.py,sha256=4tYRdckauHxkJCmOhmdwDq_eOpHPaKloi89
137
137
  schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKRcUOc41eEwKbo,2472
138
138
  schemathesis/specs/openapi/patterns.py,sha256=GqPZEXMRdWENQxanWjBOalIZ2MQUjuxk21kmdiI703E,18027
139
139
  schemathesis/specs/openapi/references.py,sha256=AW1laU23BkiRf0EEFM538vyVFLXycGUiucGVV461le0,1927
140
- schemathesis/specs/openapi/schemas.py,sha256=uTxwggtWi5dWjuKlSDs0DthlNNfcRvSJtrND-PaTkrg,33758
140
+ schemathesis/specs/openapi/schemas.py,sha256=ONFB8kMBrryZL_tKHWvxnBjyUHoHh_MAUqxjuVDc78c,34034
141
141
  schemathesis/specs/openapi/serialization.py,sha256=RPNdadne5wdhsGmjSvgKLRF58wpzpRx3wura8PsHM3o,12152
142
142
  schemathesis/specs/openapi/utils.py,sha256=XkOJT8qD-6uhq-Tmwxk_xYku1Gy5F9pKL3ldNg_DRZw,522
143
143
  schemathesis/specs/openapi/adapter/__init__.py,sha256=YEovBgLjnXd3WGPMJXq0KbSGHezkRlEv4dNRO7_evfk,249
@@ -159,17 +159,17 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=B78vps314fJOMZwlPdv7vUHo7
159
159
  schemathesis/specs/openapi/negative/mutations.py,sha256=9U352xJsdZBR-Zfy1V7_X3a5i91LIUS9Zqotrzp3BLA,21000
160
160
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
161
161
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
162
- schemathesis/specs/openapi/stateful/__init__.py,sha256=EkGiy1ksv0lH7IDBfR6Bdo1D9kaT5dLIJUwoz-p7hkw,16594
162
+ schemathesis/specs/openapi/stateful/__init__.py,sha256=RpGqyjKShp2X94obaHnCR9TO6Qt_ZAarlB-awlyMzUY,18654
163
163
  schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
164
164
  schemathesis/specs/openapi/stateful/inference.py,sha256=B99jSTDVi2yKxU7-raIb91xpacOrr0nZkEZY5Ej3eCY,9783
165
- schemathesis/specs/openapi/stateful/links.py,sha256=SSA66mU50FFBz7e6sA37CfL-Vt0OY3gont72oFSvZYU,8163
166
- schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=0JM-FrY6Awv6gl-qDHaaK7pXbt_GKutBKPyIaph8apA,7842
167
- schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=PSactImp4OqsYMHUl2gB2pgvUlZCCKJRJKeaalclFzU,11511
165
+ schemathesis/specs/openapi/stateful/links.py,sha256=TEZ7wudPdRdP-pg5XNIgYbual_9n2arBKnS2n8SxiaU,8629
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
168
168
  schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=Kl482Hwq2M8lYAdqGmf_8Yje3voSj1WLDUIujRUDWDQ,12286
169
- schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=NnXEFY1W3i18jEEYGgC_8oLoE7YOxdXgcMYtZvLj10w,12920
169
+ schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=GoO_Tw04u_7ix6qsAzMDoJooXZqIRAIV8sLphL4mGnw,13084
170
170
  schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
171
- schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=p58XoADpMKFAun0Bx_rul-kiUlfA9PXjxHJ97dT2tBE,11202
172
- schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=RaG1BJH4D7-o5Qs2rIRQvS8ERntMUEs2I5jXUFaKMRo,14147
171
+ schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=eUWE26ZixPtSuHl7laF2asS_-6qklQRhyQlk7e99hlc,12087
172
+ schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=TKM6hEuyLm5NB-jQeEbIXF7GZF-7mqYLq3OsTBRDpws,14878
173
173
  schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
174
174
  schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
175
175
  schemathesis/transport/__init__.py,sha256=6yg_RfV_9L0cpA6qpbH-SL9_3ggtHQji9CZrpIkbA6s,5321
@@ -178,8 +178,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
178
178
  schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
179
179
  schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
180
180
  schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
181
- schemathesis-4.3.11.dist-info/METADATA,sha256=yx5kZtdpuluVpHR7jIERaQlKLfLpYa7h__hOuJMAyyU,8566
182
- schemathesis-4.3.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
183
- schemathesis-4.3.11.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
184
- schemathesis-4.3.11.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
185
- schemathesis-4.3.11.dist-info/RECORD,,
181
+ schemathesis-4.3.13.dist-info/METADATA,sha256=n-G9iaIj5lgmV9oro_G7FL4f2tAeI9xOXaLe95r6WnA,8566
182
+ schemathesis-4.3.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
183
+ schemathesis-4.3.13.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
184
+ schemathesis-4.3.13.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
185
+ schemathesis-4.3.13.dist-info/RECORD,,