schemathesis 4.3.2__py3-none-any.whl → 4.3.4__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/checks.py CHANGED
@@ -81,7 +81,7 @@ class CheckContext:
81
81
 
82
82
  def _record_case(self, *, parent_id: str, case: Case) -> None:
83
83
  if self._recorder is not None:
84
- self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
84
+ self._recorder.record_case(parent_id=parent_id, case=case, transition=None, is_transition_applied=False)
85
85
 
86
86
  def _record_response(self, *, case_id: str, response: Response) -> None:
87
87
  if self._recorder is not None:
@@ -112,7 +112,8 @@ class Statistic:
112
112
  if has_failures:
113
113
  self.cases_with_failures += 1
114
114
 
115
- if case.transition is None:
115
+ # Don't report extraction failures for inferred transitions
116
+ if case.transition is None or case.transition.is_inferred:
116
117
  continue
117
118
  transition = case.transition
118
119
  parent = recorder.cases[transition.parent_id]
@@ -1048,7 +1048,11 @@ class OutputHandler(EventHandler):
1048
1048
  and event.status not in (Status.INTERRUPTED, Status.SKIP, None)
1049
1049
  ):
1050
1050
  assert self.stateful_tests_manager is not None
1051
- links_seen = {case.transition.id for case in event.recorder.cases.values() if case.transition is not None}
1051
+ links_seen = {
1052
+ case.transition.id
1053
+ for case in event.recorder.cases.values()
1054
+ if case.transition is not None and case.is_transition_applied
1055
+ }
1052
1056
  self.stateful_tests_manager.update(links_seen, event.status)
1053
1057
  self._check_stateful_warnings(ctx, event)
1054
1058
 
@@ -105,18 +105,7 @@ class EngineContext:
105
105
  InferenceAlgorithm.DEPENDENCY_ANALYSIS
106
106
  )
107
107
  ):
108
- graph = dependencies.analyze(self.schema)
109
- for response_links in graph.iter_links():
110
- operation = self.schema.get_operation_by_reference(response_links.producer_operation_ref)
111
- response = operation.responses.get(response_links.status_code)
112
- links = response.definition.setdefault(self.schema.adapter.links_keyword, {})
113
-
114
- for link_name, definition in response_links.links.items():
115
- # Find unique name if collision exists
116
- final_name = _resolve_link_name_collision(link_name, links)
117
- links[final_name] = definition.to_openapi()
118
- injected += 1
119
-
108
+ injected += dependencies.inject_links(self.schema)
120
109
  return injected
121
110
 
122
111
  def stop(self) -> None:
@@ -171,16 +160,3 @@ class EngineContext:
171
160
  kwargs["proxies"] = {"all": proxy}
172
161
  self._transport_kwargs_cache[key] = kwargs
173
162
  return kwargs
174
-
175
-
176
- def _resolve_link_name_collision(proposed_name: str, existing_links: dict[str, Any]) -> str:
177
- if proposed_name not in existing_links:
178
- return proposed_name
179
-
180
- # Name collision - find next available suffix
181
- suffix = 0
182
- while True:
183
- candidate = f"{proposed_name}_{suffix}"
184
- if candidate not in existing_links:
185
- return candidate
186
- suffix += 1
@@ -385,7 +385,7 @@ def test_func(
385
385
  transport_kwargs: dict[str, Any],
386
386
  continue_on_failure: bool,
387
387
  ) -> None:
388
- recorder.record_case(parent_id=None, transition=None, case=case)
388
+ recorder.record_case(parent_id=None, case=case, transition=None, is_transition_applied=False)
389
389
  try:
390
390
  response = case.call(**transport_kwargs)
391
391
  except (requests.Timeout, requests.ConnectionError, ChunkedEncodingError) as error:
@@ -41,9 +41,16 @@ class ScenarioRecorder:
41
41
  self.checks = {}
42
42
  self.interactions = {}
43
43
 
44
- def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
44
+ def record_case(
45
+ self, *, parent_id: str | None, case: Case, transition: Transition | None, is_transition_applied: bool
46
+ ) -> None:
45
47
  """Record a test case and its relationship to a parent, if applicable."""
46
- self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
48
+ self.cases[case.id] = CaseNode(
49
+ value=case,
50
+ parent_id=parent_id,
51
+ transition=transition,
52
+ is_transition_applied=is_transition_applied,
53
+ )
47
54
 
48
55
  def record_response(self, *, case_id: str, response: Response) -> None:
49
56
  """Record the API response for a given test case."""
@@ -139,8 +146,9 @@ class CaseNode:
139
146
  # Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
140
147
  # and outside of the implemented transition logic (e.g. Open API links)
141
148
  transition: Transition | None
149
+ is_transition_applied: bool
142
150
 
143
- __slots__ = ("value", "parent_id", "transition")
151
+ __slots__ = ("value", "parent_id", "transition", "is_transition_applied")
144
152
 
145
153
 
146
154
  @dataclass
@@ -37,12 +37,16 @@ class StepInput:
37
37
 
38
38
  case: Case
39
39
  transition: Transition | None # None for initial steps
40
+ # Whether this transition was actually applied
41
+ # Data extraction failures can prevent it, as well as transitions can be skipped in some cases
42
+ # to improve discovery of bugs triggered by non-stateful inputs during stateful testing
43
+ is_applied: bool
40
44
 
41
- __slots__ = ("case", "transition")
45
+ __slots__ = ("case", "transition", "is_applied")
42
46
 
43
47
  @classmethod
44
48
  def initial(cls, case: Case) -> StepInput:
45
- return cls(case=case, transition=None)
49
+ return cls(case=case, transition=None, is_applied=False)
46
50
 
47
51
 
48
52
  @dataclass
@@ -52,10 +56,11 @@ class Transition:
52
56
  # ID of the transition (e.g. link name)
53
57
  id: str
54
58
  parent_id: str
59
+ is_inferred: bool
55
60
  parameters: dict[str, dict[str, ExtractedParam]]
56
61
  request_body: ExtractedParam | None
57
62
 
58
- __slots__ = ("id", "parent_id", "parameters", "request_body")
63
+ __slots__ = ("id", "parent_id", "is_inferred", "parameters", "request_body")
59
64
 
60
65
 
61
66
  @dataclass
@@ -49,6 +49,8 @@ class OpenAPIStateMachine(APIStateMachine):
49
49
 
50
50
  # The proportion of negative tests generated for "root" transitions
51
51
  NEGATIVE_TEST_CASES_THRESHOLD = 10
52
+ # How often some transition is skipped
53
+ USE_TRANSITION_THRESHOLD = 85
52
54
 
53
55
 
54
56
  @dataclass
@@ -226,7 +228,7 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
226
228
  if not operation_transitions or not operation_transitions.outgoing:
227
229
  continue
228
230
 
229
- if is_likely_root_transition(operation, operation_transitions):
231
+ if is_likely_root_transition(operation):
230
232
  roots.reliable.add(operation.label)
231
233
  else:
232
234
  roots.fallback.add(operation.label)
@@ -234,7 +236,7 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
234
236
  return roots
235
237
 
236
238
 
237
- def is_likely_root_transition(operation: APIOperation, transitions: OperationTransitions) -> bool:
239
+ def is_likely_root_transition(operation: APIOperation) -> bool:
238
240
  """Check if operation is likely to succeed as a root transition."""
239
241
  # POST operations with request bodies are likely to create resources
240
242
  if operation.method == "post" and operation.body:
@@ -269,8 +271,12 @@ def into_step_input(
269
271
  and isinstance(transition.request_body.value, Ok)
270
272
  and transition.request_body.value.ok() is not UNRESOLVABLE
271
273
  and not link.merge_body
274
+ and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
272
275
  ):
273
276
  kwargs["body"] = transition.request_body.value.ok()
277
+
278
+ is_applied = bool(kwargs)
279
+
274
280
  cases = strategies.combine(
275
281
  [target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **kwargs) for mode in modes]
276
282
  )
@@ -280,24 +286,26 @@ def into_step_input(
280
286
  and isinstance(transition.request_body.value, Ok)
281
287
  and transition.request_body.value.ok() is not UNRESOLVABLE
282
288
  and link.merge_body
289
+ and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
283
290
  ):
284
291
  new = transition.request_body.value.ok()
285
292
  if isinstance(case.body, dict) and isinstance(new, dict):
286
293
  case.body = {**case.body, **new}
287
294
  else:
288
295
  case.body = new
296
+ is_applied = True
289
297
  if case.meta and case.meta.generation.mode == GenerationMode.NEGATIVE:
290
298
  # It is possible that the new body is now valid and the whole test case could be valid too
291
299
  for alternative in case.operation.body:
292
300
  if alternative.media_type == case.media_type:
293
301
  schema = alternative.optimized_schema
294
- if jsonschema.validators.validator_for(schema)(schema).is_valid(new):
302
+ if jsonschema.validators.validator_for(schema)(schema).is_valid(case.body):
295
303
  case.meta.components[ParameterLocation.BODY] = ComponentInfo(
296
304
  mode=GenerationMode.POSITIVE
297
305
  )
298
306
  if all(info.mode == GenerationMode.POSITIVE for info in case.meta.components.values()):
299
307
  case.meta.generation.mode = GenerationMode.POSITIVE
300
- return StepInput(case=case, transition=transition)
308
+ return StepInput(case=case, transition=transition, is_applied=is_applied)
301
309
 
302
310
  return inner(output=_output)
303
311
 
@@ -322,10 +330,13 @@ def transition(*, name: str, target: Bundle, input: st.SearchStrategy[StepInput]
322
330
  def step_function(self: OpenAPIStateMachine, input: StepInput) -> StepOutput | None:
323
331
  if input.transition is not None:
324
332
  self.recorder.record_case(
325
- parent_id=input.transition.parent_id, transition=input.transition, case=input.case
333
+ parent_id=input.transition.parent_id,
334
+ transition=input.transition,
335
+ case=input.case,
336
+ is_transition_applied=input.is_applied,
326
337
  )
327
338
  else:
328
- self.recorder.record_case(parent_id=None, transition=None, case=input.case)
339
+ self.recorder.record_case(parent_id=None, case=input.case, transition=None, is_transition_applied=False)
329
340
  self.control.record_step(input, self.recorder)
330
341
  return APIStateMachine._step(self, input=input)
331
342
 
@@ -5,7 +5,7 @@ Infers which operations must run before others by tracking resource creation and
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from schemathesis.core.compat import RefResolutionError
11
11
  from schemathesis.core.result import Ok
@@ -16,6 +16,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
16
16
  DefinitionSource,
17
17
  DependencyGraph,
18
18
  InputSlot,
19
+ NormalizedLink,
19
20
  OperationMap,
20
21
  OperationNode,
21
22
  OutputSlot,
@@ -25,6 +26,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
25
26
  from schemathesis.specs.openapi.stateful.dependencies.outputs import extract_outputs
26
27
 
27
28
  if TYPE_CHECKING:
29
+ from schemathesis.schemas import APIOperation
28
30
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
29
31
 
30
32
  __all__ = [
@@ -86,3 +88,85 @@ def analyze(schema: BaseOpenAPISchema) -> DependencyGraph:
86
88
  update_input_field_bindings(resource, operations)
87
89
 
88
90
  return DependencyGraph(operations=operations, resources=resources)
91
+
92
+
93
+ def inject_links(schema: BaseOpenAPISchema) -> int:
94
+ injected = 0
95
+ graph = analyze(schema)
96
+ for response_links in graph.iter_links():
97
+ operation = schema.get_operation_by_reference(response_links.producer_operation_ref)
98
+ response = operation.responses.get(response_links.status_code)
99
+ links = response.definition.setdefault(schema.adapter.links_keyword, {})
100
+
101
+ # Normalize existing links once
102
+ if links:
103
+ normalized_existing = [_normalize_link(link, schema) for link in links.values()]
104
+ else:
105
+ normalized_existing = []
106
+
107
+ for link_name, definition in response_links.links.items():
108
+ inferred_link = definition.to_openapi()
109
+
110
+ # Check if duplicate exists
111
+ if normalized_existing:
112
+ if _normalize_link(inferred_link, schema) in normalized_existing:
113
+ continue
114
+
115
+ # Find unique name if collision exists
116
+ final_name = _resolve_link_name_collision(link_name, links)
117
+ links[final_name] = inferred_link
118
+ injected += 1
119
+ return injected
120
+
121
+
122
+ def _normalize_link(link: dict[str, Any], schema: BaseOpenAPISchema) -> NormalizedLink:
123
+ """Normalize a link definition for comparison."""
124
+ operation = _resolve_link_operation(link, schema)
125
+
126
+ normalized_params = _normalize_parameter_keys(link.get("parameters", {}), operation)
127
+
128
+ return NormalizedLink(
129
+ path=operation.path,
130
+ method=operation.method,
131
+ parameters=normalized_params,
132
+ request_body=link.get("requestBody", {}),
133
+ )
134
+
135
+
136
+ def _normalize_parameter_keys(parameters: dict, operation: APIOperation) -> set[str]:
137
+ """Normalize parameter keys to location.name format."""
138
+ normalized = set()
139
+
140
+ for parameter_name in parameters.keys():
141
+ # If already has location prefix, use as-is
142
+ if "." in parameter_name:
143
+ normalized.add(parameter_name)
144
+ continue
145
+
146
+ # Find the parameter and prepend location
147
+ for parameter in operation.iter_parameters():
148
+ if parameter.name == parameter_name:
149
+ normalized.add(f"{parameter.location.value}.{parameter_name}")
150
+ break
151
+
152
+ return normalized
153
+
154
+
155
+ def _resolve_link_operation(link: dict, schema: BaseOpenAPISchema) -> APIOperation:
156
+ """Resolve link to operation, handling both operationRef and operationId."""
157
+ if "operationRef" in link:
158
+ return schema.get_operation_by_reference(link["operationRef"])
159
+ return schema.get_operation_by_id(link["operationId"])
160
+
161
+
162
+ def _resolve_link_name_collision(proposed_name: str, existing_links: dict[str, Any]) -> str:
163
+ """Find unique link name if collision exists."""
164
+ if proposed_name not in existing_links:
165
+ return proposed_name
166
+
167
+ suffix = 0
168
+ while True:
169
+ candidate = f"{proposed_name}_{suffix}"
170
+ if candidate not in existing_links:
171
+ return candidate
172
+ suffix += 1
@@ -2,6 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Iterator
4
4
 
5
+ from schemathesis.core import media_types
6
+ from schemathesis.core.errors import MalformedMediaType
7
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
5
8
  from schemathesis.core.parameters import ParameterLocation
6
9
  from schemathesis.specs.openapi.stateful.dependencies import naming
7
10
  from schemathesis.specs.openapi.stateful.dependencies.models import (
@@ -16,6 +19,7 @@ from schemathesis.specs.openapi.stateful.dependencies.resources import extract_r
16
19
 
17
20
  if TYPE_CHECKING:
18
21
  from schemathesis.core.compat import RefResolver
22
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiBody
19
23
  from schemathesis.specs.openapi.schemas import APIOperation
20
24
 
21
25
 
@@ -32,7 +36,6 @@ def extract_inputs(
32
36
  Connects each parameter (e.g., `userId`) to its resource definition (`User`),
33
37
  creating placeholder resources if not yet discovered from their schemas.
34
38
  """
35
- # Note: Currently limited to path parameters. Query / header / body will be supported in future releases.
36
39
  for param in operation.path_parameters:
37
40
  input_slot = _resolve_parameter_dependency(
38
41
  parameter_name=param.name,
@@ -46,6 +49,13 @@ def extract_inputs(
46
49
  if input_slot is not None:
47
50
  yield input_slot
48
51
 
52
+ for body in operation.body:
53
+ try:
54
+ if media_types.is_json(body.media_type):
55
+ yield from _resolve_body_dependencies(body=body, operation=operation, resources=resources)
56
+ except MalformedMediaType:
57
+ continue
58
+
49
59
 
50
60
  def _resolve_parameter_dependency(
51
61
  *,
@@ -152,6 +162,52 @@ def _find_resource_in_responses(
152
162
  return None
153
163
 
154
164
 
165
+ def _resolve_body_dependencies(
166
+ *, body: OpenApiBody, operation: APIOperation, resources: ResourceMap
167
+ ) -> Iterator[InputSlot]:
168
+ schema = body.raw_schema
169
+ if not isinstance(schema, dict):
170
+ return
171
+
172
+ # Right now, the body schema comes bundled to dependency analysis
173
+ if BUNDLE_STORAGE_KEY in schema and "$ref" in schema:
174
+ schema_key = schema["$ref"].split("/")[-1]
175
+ resolved = schema[BUNDLE_STORAGE_KEY][schema_key]
176
+ else:
177
+ resolved = schema
178
+
179
+ # Inspect each property that could be a part of some other resource
180
+ properties = resolved.get("properties", {})
181
+ path = operation.path
182
+ for property_name in properties:
183
+ resource_name = naming.from_parameter(property_name, path)
184
+ if resource_name is None:
185
+ continue
186
+ resource = resources.get(resource_name)
187
+ if resource is None:
188
+ resource = ResourceDefinition.inferred_from_parameter(
189
+ name=resource_name,
190
+ parameter_name=property_name,
191
+ )
192
+ resources[resource_name] = resource
193
+ field = property_name
194
+ else:
195
+ field = (
196
+ naming.find_matching_field(
197
+ parameter=property_name,
198
+ resource=resource_name,
199
+ fields=resource.fields,
200
+ )
201
+ or "id"
202
+ )
203
+ yield InputSlot(
204
+ resource=resource,
205
+ resource_field=field,
206
+ parameter_name=property_name,
207
+ parameter_location=ParameterLocation.BODY,
208
+ )
209
+
210
+
155
211
  def update_input_field_bindings(resource_name: str, operations: OperationMap) -> None:
156
212
  """Update input slots field bindings after resource definition was upgraded.
157
213
 
@@ -9,6 +9,7 @@ from typing_extensions import TypeAlias
9
9
 
10
10
  from schemathesis.core.parameters import ParameterLocation
11
11
  from schemathesis.core.transforms import encode_pointer
12
+ from schemathesis.specs.openapi.stateful.links import SCHEMATHESIS_LINK_EXTENSION
12
13
 
13
14
 
14
15
  @dataclass
@@ -61,12 +62,21 @@ class DependencyGraph:
61
62
  output_slot.pointer, input_slot.resource_field, output_slot.cardinality
62
63
  )
63
64
  link_name = f"{consumer.method.capitalize()}{input_slot.resource.name}"
65
+ parameters = {}
66
+ request_body = {}
67
+ # Data is extracted from response body
68
+ if input_slot.parameter_location == ParameterLocation.BODY:
69
+ request_body = {
70
+ input_slot.parameter_name: f"$response.body#{body_pointer}",
71
+ }
72
+ else:
73
+ parameters = {
74
+ f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
75
+ }
64
76
  links[link_name] = LinkDefinition(
65
77
  operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
66
- parameters={
67
- # Data is extracted from response body
68
- f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
69
- },
78
+ parameters=parameters,
79
+ request_body=request_body,
70
80
  )
71
81
  if links:
72
82
  yield ResponseLinks(
@@ -136,14 +146,23 @@ class LinkDefinition:
136
146
  parameters: dict[str, str]
137
147
  """Parameter mappings (e.g., {'path.id': '$response.body#/id'})"""
138
148
 
139
- __slots__ = ("operation_ref", "parameters")
149
+ request_body: dict[str, str]
150
+ """Request body (e.g., {'path.id': '$response.body#/id'})"""
151
+
152
+ __slots__ = ("operation_ref", "parameters", "request_body")
140
153
 
141
154
  def to_openapi(self) -> dict[str, Any]:
142
155
  """Convert to OpenAPI Links format."""
143
- return {
156
+ links: dict[str, Any] = {
144
157
  "operationRef": self.operation_ref,
145
- "parameters": self.parameters,
158
+ SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True},
146
159
  }
160
+ if self.parameters:
161
+ links["parameters"] = self.parameters
162
+ if self.request_body:
163
+ links["requestBody"] = self.request_body
164
+ links[SCHEMATHESIS_LINK_EXTENSION]["merge_body"] = True
165
+ return links
147
166
 
148
167
 
149
168
  @dataclass
@@ -177,6 +196,18 @@ class ResponseLinks:
177
196
  return {name: link_def.to_openapi() for name, link_def in self.links.items()}
178
197
 
179
198
 
199
+ @dataclass
200
+ class NormalizedLink:
201
+ """Normalized representation of a link."""
202
+
203
+ path: str
204
+ method: str
205
+ parameters: set[str]
206
+ request_body: Any
207
+
208
+ __slots__ = ("path", "method", "parameters", "request_body")
209
+
210
+
180
211
  class Cardinality(str, enum.Enum):
181
212
  """Whether there is one or many resources in a slot."""
182
213
 
@@ -27,76 +27,122 @@ def from_path(path: str) -> str | None:
27
27
 
28
28
 
29
29
  IRREGULAR_TO_PLURAL = {
30
- "echo": "echoes",
31
- "dingo": "dingoes",
32
- "volcano": "volcanoes",
33
- "tornado": "tornadoes",
34
- "torpedo": "torpedoes",
35
- "genus": "genera",
36
- "viscus": "viscera",
37
- "stigma": "stigmata",
38
- "stoma": "stomata",
39
- "dogma": "dogmata",
40
- "lemma": "lemmata",
30
+ "abuse": "abuses",
31
+ "alias": "aliases",
32
+ "analysis": "analyses",
41
33
  "anathema": "anathemata",
42
- "ox": "oxen",
43
34
  "axe": "axes",
35
+ "base": "bases",
36
+ "bookshelf": "bookshelves",
37
+ "cache": "caches",
38
+ "canvas": "canvases",
39
+ "carve": "carves",
40
+ "case": "cases",
41
+ "cause": "causes",
42
+ "child": "children",
43
+ "course": "courses",
44
+ "criterion": "criteria",
45
+ "database": "databases",
46
+ "defense": "defenses",
47
+ "diagnosis": "diagnoses",
44
48
  "die": "dice",
45
- "yes": "yeses",
46
- "foot": "feet",
49
+ "dingo": "dingoes",
50
+ "disease": "diseases",
51
+ "dogma": "dogmata",
52
+ "dose": "doses",
47
53
  "eave": "eaves",
54
+ "echo": "echoes",
55
+ "enterprise": "enterprises",
56
+ "ephemeris": "ephemerides",
57
+ "excuse": "excuses",
58
+ "expense": "expenses",
59
+ "foot": "feet",
60
+ "franchise": "franchises",
61
+ "genus": "genera",
48
62
  "goose": "geese",
49
- "tooth": "teeth",
50
- "quiz": "quizzes",
51
- "human": "humans",
52
- "proof": "proofs",
53
- "carve": "carves",
54
- "valve": "valves",
55
- "looey": "looies",
56
- "thief": "thieves",
57
63
  "groove": "grooves",
58
- "pickaxe": "pickaxes",
59
- "passerby": "passersby",
60
- "canvas": "canvases",
61
- "use": "uses",
62
- "case": "cases",
63
- "vase": "vases",
64
+ "half": "halves",
65
+ "horse": "horses",
64
66
  "house": "houses",
67
+ "human": "humans",
68
+ "hypothesis": "hypotheses",
69
+ "index": "indices",
70
+ "knife": "knives",
71
+ "lemma": "lemmata",
72
+ "license": "licenses",
73
+ "life": "lives",
74
+ "loaf": "loaves",
75
+ "looey": "looies",
76
+ "man": "men",
77
+ "matrix": "matrices",
65
78
  "mouse": "mice",
66
- "reuse": "reuses",
67
- "abuse": "abuses",
68
- "excuse": "excuses",
69
- "cause": "causes",
79
+ "movie": "movies",
80
+ "nose": "noses",
81
+ "oasis": "oases",
82
+ "ox": "oxen",
83
+ "passerby": "passersby",
70
84
  "pause": "pauses",
71
- "base": "bases",
85
+ "person": "people",
72
86
  "phase": "phases",
73
- "rose": "roses",
74
- "dose": "doses",
75
- "nose": "noses",
76
- "horse": "horses",
77
- "course": "courses",
87
+ "phenomenon": "phenomena",
88
+ "pickaxe": "pickaxes",
89
+ "proof": "proofs",
90
+ "purchase": "purchases",
78
91
  "purpose": "purposes",
92
+ "quiz": "quizzes",
93
+ "radius": "radii",
94
+ "release": "releases",
79
95
  "response": "responses",
96
+ "reuse": "reuses",
97
+ "rose": "roses",
98
+ "scarf": "scarves",
99
+ "self": "selves",
80
100
  "sense": "senses",
101
+ "shelf": "shelves",
102
+ "size": "sizes",
103
+ "snooze": "snoozes",
104
+ "stigma": "stigmata",
105
+ "stoma": "stomata",
106
+ "synopsis": "synopses",
81
107
  "tense": "tenses",
82
- "expense": "expenses",
83
- "license": "licenses",
84
- "defense": "defenses",
108
+ "thief": "thieves",
109
+ "tooth": "teeth",
110
+ "tornado": "tornadoes",
111
+ "torpedo": "torpedoes",
112
+ "use": "uses",
113
+ "valve": "valves",
114
+ "vase": "vases",
115
+ "verse": "verses",
116
+ "viscus": "viscera",
117
+ "volcano": "volcanoes",
118
+ "warehouse": "warehouses",
119
+ "wave": "waves",
120
+ "wife": "wives",
121
+ "wolf": "wolves",
122
+ "woman": "women",
123
+ "yes": "yeses",
124
+ "vie": "vies",
85
125
  }
86
126
  IRREGULAR_TO_SINGULAR = {v: k for k, v in IRREGULAR_TO_PLURAL.items()}
87
127
  UNCOUNTABLE = frozenset(
88
128
  [
129
+ "access",
130
+ "address",
89
131
  "adulthood",
90
132
  "advice",
91
133
  "agenda",
92
134
  "aid",
93
135
  "aircraft",
94
136
  "alcohol",
137
+ "alias",
95
138
  "ammo",
139
+ "analysis",
96
140
  "analytics",
97
141
  "anime",
142
+ "anonymous",
98
143
  "athletics",
99
144
  "audio",
145
+ "bias",
100
146
  "bison",
101
147
  "blood",
102
148
  "bream",
@@ -104,22 +150,31 @@ UNCOUNTABLE = frozenset(
104
150
  "butter",
105
151
  "carp",
106
152
  "cash",
153
+ "chaos",
107
154
  "chassis",
108
155
  "chess",
109
156
  "clothing",
110
157
  "cod",
111
158
  "commerce",
159
+ "compass",
160
+ "consensus",
112
161
  "cooperation",
113
162
  "corps",
163
+ "data",
114
164
  "debris",
165
+ "deer",
115
166
  "diabetes",
167
+ "diagnosis",
116
168
  "digestion",
117
169
  "elk",
118
170
  "energy",
171
+ "ephemeris",
119
172
  "equipment",
173
+ "eries",
120
174
  "excretion",
121
175
  "expertise",
122
176
  "firmware",
177
+ "fish",
123
178
  "flounder",
124
179
  "fun",
125
180
  "gallows",
@@ -141,12 +196,15 @@ UNCOUNTABLE = frozenset(
141
196
  "machinery",
142
197
  "mackerel",
143
198
  "mail",
199
+ "manga",
200
+ "means",
144
201
  "media",
202
+ "metadata",
145
203
  "mews",
204
+ "money",
146
205
  "moose",
147
- "music",
148
206
  "mud",
149
- "manga",
207
+ "music",
150
208
  "news",
151
209
  "only",
152
210
  "personnel",
@@ -156,6 +214,9 @@ UNCOUNTABLE = frozenset(
156
214
  "police",
157
215
  "pollution",
158
216
  "premises",
217
+ "progress",
218
+ "prometheus",
219
+ "radius",
159
220
  "rain",
160
221
  "research",
161
222
  "rice",
@@ -164,10 +225,13 @@ UNCOUNTABLE = frozenset(
164
225
  "series",
165
226
  "sewage",
166
227
  "shambles",
228
+ "sheep",
167
229
  "shrimp",
168
230
  "software",
231
+ "species",
169
232
  "staff",
170
233
  "swine",
234
+ "synopsis",
171
235
  "tennis",
172
236
  "traffic",
173
237
  "transportation",
@@ -178,22 +242,37 @@ UNCOUNTABLE = frozenset(
178
242
  "whiting",
179
243
  "wildebeest",
180
244
  "wildlife",
245
+ "wireless",
181
246
  "you",
182
- "sheep",
183
- "deer",
184
- "species",
185
- "series",
186
- "means",
187
247
  ]
188
248
  )
189
249
 
190
250
 
251
+ def _is_word_like(s: str) -> bool:
252
+ """Check if string looks like a word (not a path, technical term, etc)."""
253
+ # Skip empty or very short
254
+ if not s or len(s) < 2:
255
+ return False
256
+ # Skip if contains non-word characters (except underscore and hyphen)
257
+ if not all(c.isalpha() or c in ("_", "-") for c in s):
258
+ return False
259
+ # Skip if has numbers
260
+ return not any(c.isdigit() for c in s)
261
+
262
+
191
263
  def to_singular(word: str) -> str:
192
- if word in UNCOUNTABLE:
264
+ if not _is_word_like(word):
265
+ return word
266
+ if word.lower() in UNCOUNTABLE:
267
+ return word
268
+ known_lower = IRREGULAR_TO_SINGULAR.get(word.lower())
269
+ if known_lower is not None:
270
+ # Preserve case: if input was capitalized, capitalize result
271
+ if word[0].isupper():
272
+ return known_lower.capitalize()
273
+ return known_lower
274
+ if word.endswith(("ss", "us")):
193
275
  return word
194
- known = IRREGULAR_TO_SINGULAR.get(word)
195
- if known is not None:
196
- return known
197
276
  if word.endswith("ies") and len(word) > 3 and word[-4] not in "aeiou":
198
277
  return word[:-3] + "y"
199
278
  if word.endswith("sses"):
@@ -211,11 +290,18 @@ def to_singular(word: str) -> str:
211
290
 
212
291
 
213
292
  def to_plural(word: str) -> str:
214
- if word in UNCOUNTABLE:
293
+ if not _is_word_like(word):
294
+ return word
295
+ if word.lower() in UNCOUNTABLE:
215
296
  return word
216
297
  known = IRREGULAR_TO_PLURAL.get(word)
217
298
  if known is not None:
218
299
  return known
300
+ known_lower = IRREGULAR_TO_PLURAL.get(word.lower())
301
+ if known_lower is not None:
302
+ if word[0].isupper():
303
+ return known_lower.capitalize()
304
+ return known_lower
219
305
  # Only change y -> ies after consonants (party -> parties, not day -> days)
220
306
  if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
221
307
  return word[:-1] + "ies"
@@ -3,14 +3,6 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any, Callable, Mapping
5
5
 
6
- from hypothesis_jsonschema._canonicalise import (
7
- SCHEMA_KEYS as SCHEMA_KEYS_TUPLE,
8
- )
9
- from hypothesis_jsonschema._canonicalise import (
10
- SCHEMA_OBJECT_KEYS as SCHEMA_OBJECT_KEYS_TUPLE,
11
- )
12
- from hypothesis_jsonschema._canonicalise import canonicalish, merged
13
-
14
6
  from schemathesis.core.jsonschema import ALL_KEYWORDS
15
7
  from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY, bundle
16
8
  from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject
@@ -23,8 +15,23 @@ if TYPE_CHECKING:
23
15
  from schemathesis.core.compat import RefResolver
24
16
 
25
17
  ROOT_POINTER = "/"
26
- SCHEMA_KEYS = frozenset(SCHEMA_KEYS_TUPLE)
27
- SCHEMA_OBJECT_KEYS = frozenset(SCHEMA_OBJECT_KEYS_TUPLE)
18
+ SCHEMA_KEYS = frozenset(
19
+ {
20
+ "propertyNames",
21
+ "contains",
22
+ "if",
23
+ "items",
24
+ "oneOf",
25
+ "anyOf",
26
+ "additionalProperties",
27
+ "then",
28
+ "else",
29
+ "not",
30
+ "additionalItems",
31
+ "allOf",
32
+ }
33
+ )
34
+ SCHEMA_OBJECT_KEYS = frozenset({"dependencies", "properties", "patternProperties"})
28
35
 
29
36
 
30
37
  def resolve_all_refs(schema: JsonSchemaObject) -> dict[str, Any]:
@@ -48,6 +55,8 @@ def resolve_all_refs(schema: JsonSchemaObject) -> dict[str, Any]:
48
55
 
49
56
 
50
57
  def resolve_all_refs_inner(schema: JsonSchema, *, resolve: Callable[[str], dict[str, Any]]) -> dict[str, Any]:
58
+ from hypothesis_jsonschema._canonicalise import merged
59
+
51
60
  if schema is True:
52
61
  return {}
53
62
  if schema is False:
@@ -80,6 +89,8 @@ def resolve_all_refs_inner(schema: JsonSchema, *, resolve: Callable[[str], dict[
80
89
 
81
90
  def canonicalize(schema: dict[str, Any], resolver: RefResolver) -> Mapping[str, Any]:
82
91
  """Transform the input schema into its canonical-ish form."""
92
+ from hypothesis_jsonschema._canonicalise import canonicalish
93
+
83
94
  # Canonicalisation in `hypothesis_jsonschema` requires all references to be resovable and non-recursive
84
95
  # On the Schemathesis side bundling solves this problem
85
96
  bundled = bundle(schema, resolver, inline_recursive=True)
@@ -22,6 +22,7 @@ from werkzeug.routing import Map, MapAdapter, Rule
22
22
 
23
23
  from schemathesis.core.adapter import ResponsesContainer
24
24
  from schemathesis.core.transforms import encode_pointer
25
+ from schemathesis.specs.openapi.stateful.links import SCHEMATHESIS_LINK_EXTENSION
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from schemathesis.engine.observations import LocationHeaderEntry
@@ -39,7 +40,7 @@ class OperationById:
39
40
  __slots__ = ("value", "method", "path")
40
41
 
41
42
  def to_link_base(self) -> dict[str, Any]:
42
- return {"operationId": self.value, "x-inferred": True}
43
+ return {"operationId": self.value, SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True}}
43
44
 
44
45
 
45
46
  @dataclass(unsafe_hash=True)
@@ -53,7 +54,7 @@ class OperationByRef:
53
54
  __slots__ = ("value", "method", "path")
54
55
 
55
56
  def to_link_base(self) -> dict[str, Any]:
56
- return {"operationRef": self.value, "x-inferred": True}
57
+ return {"operationRef": self.value, SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True}}
57
58
 
58
59
 
59
60
  OperationReference = Union[OperationById, OperationByRef]
@@ -38,8 +38,19 @@ class OpenApiLink:
38
38
  parameters: list[NormalizedParameter]
39
39
  body: dict[str, Any] | NotSet
40
40
  merge_body: bool
41
-
42
- __slots__ = ("name", "status_code", "source", "target", "parameters", "body", "merge_body", "_cached_extract")
41
+ is_inferred: bool
42
+
43
+ __slots__ = (
44
+ "name",
45
+ "status_code",
46
+ "source",
47
+ "target",
48
+ "parameters",
49
+ "body",
50
+ "merge_body",
51
+ "is_inferred",
52
+ "_cached_extract",
53
+ )
43
54
 
44
55
  def __init__(self, name: str, status_code: str, definition: dict[str, Any], source: APIOperation):
45
56
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
@@ -69,6 +80,7 @@ class OpenApiLink:
69
80
  self.parameters = self._normalize_parameters(definition.get("parameters", {}), errors)
70
81
  self.body = definition.get("requestBody", NOT_SET)
71
82
  self.merge_body = extension.get("merge_body", True) if extension else True
83
+ self.is_inferred = extension.get("is_inferred", False) if extension else False
72
84
 
73
85
  if errors:
74
86
  raise InvalidTransition(
@@ -148,6 +160,7 @@ class OpenApiLink:
148
160
  return Transition(
149
161
  id=f"{self.source.label} -> [{self.status_code}] {self.name} -> {self.target.label}",
150
162
  parent_id=output.case.id,
163
+ is_inferred=self.is_inferred,
151
164
  parameters=self.extract_parameters(output),
152
165
  request_body=self.extract_body(output),
153
166
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.3.2
3
+ Version: 4.3.4
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
@@ -1,6 +1,6 @@
1
1
  schemathesis/__init__.py,sha256=QqVUCBQr-RDEstgCZLsxzIa9HJslVSeijrm9gES4b_0,1423
2
2
  schemathesis/auths.py,sha256=JdEwPRS9WKmPcxzGXYYz9pjlIUMQYCfif7ZJU0Kde-I,16400
3
- schemathesis/checks.py,sha256=GTdejjXDooAOuq66nvCK3i-AMPBuU-_-aNeSeL9JIlc,6561
3
+ schemathesis/checks.py,sha256=yyR219TQjLAieop5O2waEVLILF6_i10B8mkutd6QaCs,6590
4
4
  schemathesis/errors.py,sha256=K3irHIZkrBH2-9LIjlgXlm8RNC41Nffd39ncfwagUvw,1053
5
5
  schemathesis/filters.py,sha256=IevPA5A04GfRLLjmkFLZ0CLhjNO3RmpZq_yw6MqjLIA,13515
6
6
  schemathesis/hooks.py,sha256=q2wqYNgpMCO8ImSBkbrWDSwN0BSELelqJMgAAgGvv2M,14836
@@ -13,7 +13,7 @@ schemathesis/cli/core.py,sha256=ue7YUdVo3YvuzGL4s6i62NL6YqNDeVPBSnQ1znrvG2w,480
13
13
  schemathesis/cli/commands/__init__.py,sha256=DNzKEnXu7GjGSVe0244ZErmygUBA3nGSyVY6JP3ixD0,3740
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
- schemathesis/cli/commands/run/context.py,sha256=taegOHWc_B-HDwiU1R9Oi4q57mdfLXc-B954QUj8t7A,7984
16
+ schemathesis/cli/commands/run/context.py,sha256=Usa89aSPf8Uv-2m-nWr0ghvTKM1ZZehALBI0m_lFHv4,8087
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
@@ -23,7 +23,7 @@ schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31M
23
23
  schemathesis/cli/commands/run/handlers/base.py,sha256=qUtDvtr3F6were_BznfnaPpMibGJMnQ5CA9aEzcIUBc,1306
24
24
  schemathesis/cli/commands/run/handlers/cassettes.py,sha256=LzvQp--Ub5MXF7etet7fQD0Ufloh1R0j2X1o9dT8Z4k,19253
25
25
  schemathesis/cli/commands/run/handlers/junitxml.py,sha256=qiFvM4-SlM67sep003SkLqPslzaEb4nOm3bkzw-DO-Q,2602
26
- schemathesis/cli/commands/run/handlers/output.py,sha256=TwK82zNpIZ7Q76ggTp8gcW2clzrw0WBmHFJMcvYL1nE,63927
26
+ schemathesis/cli/commands/run/handlers/output.py,sha256=jWrqEkEQPO2kgzxOffZacqxH6r7dkDmAx0ep9GA3NU8,64020
27
27
  schemathesis/cli/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  schemathesis/cli/ext/fs.py,sha256=dHQYBjQozQmuSSfXVp-2KWFK0ESOb_w-lV2SptfMfco,461
29
29
  schemathesis/cli/ext/groups.py,sha256=kQ37t6qeArcKaY2y5VxyK3_KwAkBKCVm58IYV8gewds,2720
@@ -76,20 +76,20 @@ schemathesis/core/jsonschema/types.py,sha256=C7f9g8yKFuoxC5_0YNIh8QAyGU0-tj8pzTM
76
76
  schemathesis/core/output/__init__.py,sha256=SiHqONFskXl73AtP5dV29L14nZoKo7B-IeG52KZB32M,1446
77
77
  schemathesis/core/output/sanitization.py,sha256=Ev3tae8dVwsYd7yVb2_1VBFYs92WFsQ4Eu1fGaymItE,2013
78
78
  schemathesis/engine/__init__.py,sha256=QaFE-FinaTAaarteADo2RRMJ-Sz6hZB9TzD5KjMinIA,706
79
- schemathesis/engine/context.py,sha256=iMyyum60AmZlX1reghxzCW6A_dDA43RA3NXJZqruHv8,6821
79
+ schemathesis/engine/context.py,sha256=YaBfwTUyTCZaMq7-jtAKFQj-Eh1aQdbZ0UNcC5d_epU,5792
80
80
  schemathesis/engine/control.py,sha256=FXzP8dxL47j1Giqpy2-Bsr_MdMw9YiATSK_UfpFwDtk,1348
81
81
  schemathesis/engine/core.py,sha256=qlPHnZVq2RrUe93fOciXd1hC3E1gVyF2BIWMPMeLIj8,6655
82
82
  schemathesis/engine/errors.py,sha256=FlpEk44WRLzRkdK9m37z93EQuY3kbeMIQRGwU5e3Qm4,19005
83
83
  schemathesis/engine/events.py,sha256=jpCtMkWWfNe2jUeZh_Ly_wfZEF44EOodL-I_W4C9rgg,6594
84
84
  schemathesis/engine/observations.py,sha256=T-5R8GeVIqvxpCMxc6vZ04UUxUTx3w7689r3Dc6bIcE,1416
85
- schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
85
+ schemathesis/engine/recorder.py,sha256=KWyWkGkZxIwSDU92jNWCJXU4G4E5WqfhLM6G1Yi7Jyo,8636
86
86
  schemathesis/engine/phases/__init__.py,sha256=7Yp7dQbd6-K9pavIJeURg6jiNeMpW8UU-Iiikr668ts,3278
87
87
  schemathesis/engine/phases/probes.py,sha256=YogjJcZJcTMS8sMdGnG4oXKmMUj_4r_J7MY-BBJtCRU,5690
88
88
  schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuUI5bh4L-RIFexU1-c_Q,2461
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=UkOcmR23rl5PSIuNMjthZxadbAOHYSmTWft7Wa91yLs,17206
92
+ schemathesis/engine/phases/unit/_executor.py,sha256=YDibV3lkC2UMHLvh1FSmnlaQ-SJS-R0MU2qEF4NBbf0,17235
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=Qc2_5JrWuUkCzAFTTgnVqNUJ2sioslmINTXiY7nHHgA,12326
@@ -105,7 +105,7 @@ schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5
105
105
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
106
106
  schemathesis/generation/hypothesis/strategies.py,sha256=RurE81E06d99YKG48dizy9346ayfNswYTt38zewmGgw,483
107
107
  schemathesis/generation/stateful/__init__.py,sha256=s7jiJEnguIj44IsRyMi8afs-8yjIUuBbzW58bH5CHjs,1042
108
- schemathesis/generation/stateful/state_machine.py,sha256=3oAGpn46adNJx3sp5ym9e30SyYOjJGiEqenDZ5gWtBY,8803
108
+ schemathesis/generation/stateful/state_machine.py,sha256=25kkYImw5byNwuTtt97aNE3kTHAF8rZ-p3ax_bmd3JI,9135
109
109
  schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
110
110
  schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
111
111
  schemathesis/graphql/loaders.py,sha256=2tgG4HIvFmjHLr_KexVXnT8hSBM-dKG_fuXTZgE97So,9445
@@ -160,17 +160,17 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=B78vps314fJOMZwlPdv7vUHo7
160
160
  schemathesis/specs/openapi/negative/mutations.py,sha256=9U352xJsdZBR-Zfy1V7_X3a5i91LIUS9Zqotrzp3BLA,21000
161
161
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
162
162
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
163
- schemathesis/specs/openapi/stateful/__init__.py,sha256=nD5f9pP2Rx2DKIeXtbc_KqUukC4Nf2834cHeOp52byM,16247
163
+ schemathesis/specs/openapi/stateful/__init__.py,sha256=CQx2WJ3mKn5qmYRc90DqsG9w3Gx7DrB60S9HFz81STY,16663
164
164
  schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
165
- schemathesis/specs/openapi/stateful/inference.py,sha256=9o9V-UUpphW7u_Kqz5MCp1_JXS2H_rcAZwz0bwJnmbI,9637
166
- schemathesis/specs/openapi/stateful/links.py,sha256=G6vqW6JFOdhF044ZjG6PsSwAHU1yP4E3FolcNFE55NM,7918
167
- schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=epBYtVw7q9mkV-UtlJNbfJQgwAs9d5jkOJYkyEeUMvE,3348
168
- schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=5niLSz7-wl-sP9cJdnYBItwEeLuzJr3nw4XIMT3yt98,6764
169
- schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=4MXJzCat1bU-tmwAM7OH2dFBys_YHCJw9wTgd9Hib3c,9728
170
- schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=1aaa8vc56tsyOAKAvD8oPk55S-qbzrCBYe1kCk3Y9VY,9052
165
+ 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=IE8WIeWNhQoNCwiivQKoDe3GD_aobxmjQYvarwxp_1M,6379
168
+ schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=DJDDCq73OYvCIPMxLKXJGTQGloNf6z6mgxjzjD0kJHA,8739
169
+ schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=s8_RBwpciAmPMISp5WDabqEuX7dXW84S-QWnhkodz6g,10938
170
+ schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=MGoyh1bfw2SoKzdbzpHxed9LHMjokPJTU_YErZaF-Ls,11396
171
171
  schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
172
172
  schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=7E2Z6LvomSRrp_0vCD_adzoux0wBLEjKi_EiSqiN43U,9664
173
- schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=pNV2GibNW8042KrdfUQBdJEkGj_dd84bTHbqunba48k,13976
173
+ schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=yMu13RsXIPDeZT1tATTxI1vkpYhjs-XFSFEvx3_Xh_Q,14094
174
174
  schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
175
175
  schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
176
176
  schemathesis/transport/__init__.py,sha256=6yg_RfV_9L0cpA6qpbH-SL9_3ggtHQji9CZrpIkbA6s,5321
@@ -179,8 +179,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
179
179
  schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
180
180
  schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
181
181
  schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
182
- schemathesis-4.3.2.dist-info/METADATA,sha256=t4XVoDj7Tst0-FT19wxnLdQ3wVtaveCp18GMoa1ZzU4,8540
183
- schemathesis-4.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
- schemathesis-4.3.2.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
- schemathesis-4.3.2.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
- schemathesis-4.3.2.dist-info/RECORD,,
182
+ schemathesis-4.3.4.dist-info/METADATA,sha256=Su_dAAs7RpHERfreFwrore0Nx6LqQJQB60HlC-C8TAQ,8540
183
+ schemathesis-4.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
+ schemathesis-4.3.4.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
+ schemathesis-4.3.4.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
+ schemathesis-4.3.4.dist-info/RECORD,,