schemathesis 4.3.3__py3-none-any.whl → 4.3.5__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,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from dataclasses import dataclass, field
4
5
  from typing import TYPE_CHECKING, Callable, Generator
5
6
 
@@ -112,7 +113,8 @@ class Statistic:
112
113
  if has_failures:
113
114
  self.cases_with_failures += 1
114
115
 
115
- if case.transition is None:
116
+ # Don't report extraction failures for inferred transitions
117
+ if case.transition is None or case.transition.is_inferred:
116
118
  continue
117
119
  transition = case.transition
118
120
  parent = recorder.cases[transition.parent_id]
@@ -120,10 +122,32 @@ class Statistic:
120
122
  # We need a response to get there, so it should be present
121
123
  assert response is not None
122
124
 
125
+ history = None
126
+
127
+ if (
128
+ transition.request_body is not None
129
+ and isinstance(transition.request_body.value, Ok)
130
+ and transition.request_body.value.ok() is UNRESOLVABLE
131
+ ):
132
+ history = collect_history(parent, response)
133
+ extraction_failures.add(
134
+ ExtractionFailure(
135
+ id=transition.id,
136
+ case_id=case_id,
137
+ source=parent.value.operation.label,
138
+ target=case.value.operation.label,
139
+ parameter_name="body",
140
+ expression=json.dumps(transition.request_body.definition),
141
+ history=history,
142
+ response=response,
143
+ error=None,
144
+ )
145
+ )
146
+
123
147
  for params in transition.parameters.values():
124
148
  for parameter, extracted in params.items():
125
149
  if isinstance(extracted.value, Ok) and extracted.value.ok() is UNRESOLVABLE:
126
- history = collect_history(parent, response)
150
+ history = history or collect_history(parent, response)
127
151
  extraction_failures.add(
128
152
  ExtractionFailure(
129
153
  id=transition.id,
@@ -138,7 +162,7 @@ class Statistic:
138
162
  )
139
163
  )
140
164
  elif isinstance(extracted.value, Err):
141
- history = collect_history(parent, response)
165
+ history = history or collect_history(parent, response)
142
166
  extraction_failures.add(
143
167
  ExtractionFailure(
144
168
  id=transition.id,
@@ -1295,9 +1295,10 @@ class OutputHandler(EventHandler):
1295
1295
  else:
1296
1296
  click.echo(f"\n{indent}{failure.error.__class__.__name__}: {failure.error}")
1297
1297
  else:
1298
- description = (
1299
- f"\n{indent}Could not resolve parameter `{failure.parameter_name}` via `{failure.expression}`"
1300
- )
1298
+ if failure.parameter_name == "body":
1299
+ description = f"\n{indent}Could not resolve request body via {failure.expression}"
1300
+ else:
1301
+ description = f"\n{indent}Could not resolve parameter `{failure.parameter_name}` via `{failure.expression}`"
1301
1302
  prefix = "$response.body"
1302
1303
  if failure.expression.startswith(prefix):
1303
1304
  description += f"\n{indent}Path `{failure.expression[len(prefix) :]}` not found in response"
@@ -41,6 +41,7 @@ class Bundler:
41
41
  return schema
42
42
 
43
43
  # Track visited URIs and their local definition names
44
+ inlining_for_recursion: set[str] = set()
44
45
  visited: set[str] = set()
45
46
  uri_to_def_name: dict[str, str] = {}
46
47
  defs = {}
@@ -90,19 +91,26 @@ class Bundler:
90
91
  # L is the number of levels. Even quadratic growth can be unacceptable for large schemas.
91
92
  #
92
93
  # In the future, it **should** be handled by `hypothesis-jsonschema` instead.
93
- cloned = deepclone(resolved_schema)
94
- remaining_references = sanitize(cloned)
95
- if reference in remaining_references:
96
- # This schema is either infinitely recursive or the sanitization logic misses it
94
+ if resolved_uri in inlining_for_recursion:
95
+ # Check if we're already trying to inline this schema
96
+ # If yes, it means we have an unbreakable cycle
97
97
  cycle = scopes[scopes.index(resolved_uri) :]
98
98
  raise InfiniteRecursiveReference(reference, cycle)
99
99
 
100
- result = {key: _bundle_recursive(value) for key, value in current.items() if key != "$ref"}
101
- # Recursive references need `$ref` to be in them, which is only possible with `dict`
102
- bundled_clone = _bundle_recursive(cloned)
103
- assert isinstance(bundled_clone, dict)
104
- result.update(bundled_clone)
105
- return result
100
+ # Track that we're inlining this schema
101
+ inlining_for_recursion.add(resolved_uri)
102
+ try:
103
+ cloned = deepclone(resolved_schema)
104
+ # Sanitize to remove optional recursive references
105
+ sanitize(cloned)
106
+
107
+ result = {key: _bundle_recursive(value) for key, value in current.items() if key != "$ref"}
108
+ bundled_clone = _bundle_recursive(cloned)
109
+ assert isinstance(bundled_clone, dict)
110
+ result.update(bundled_clone)
111
+ return result
112
+ finally:
113
+ inlining_for_recursion.discard(resolved_uri)
106
114
  elif resolved_uri not in visited:
107
115
  # Bundle only new schemas
108
116
  visit(resolved_uri)
@@ -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
@@ -530,7 +530,6 @@ def _iter_coverage_cases(
530
530
  instant = Instant()
531
531
  responses = list(operation.responses.iter_examples())
532
532
  # NOTE: The HEAD method is excluded
533
- unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
534
533
  custom_formats = _build_custom_formats(generation_config)
535
534
 
536
535
  seen_negative = coverage.HashSet()
@@ -56,10 +56,11 @@ class Transition:
56
56
  # ID of the transition (e.g. link name)
57
57
  id: str
58
58
  parent_id: str
59
+ is_inferred: bool
59
60
  parameters: dict[str, dict[str, ExtractedParam]]
60
61
  request_body: ExtractedParam | None
61
62
 
62
- __slots__ = ("id", "parent_id", "parameters", "request_body")
63
+ __slots__ = ("id", "parent_id", "is_inferred", "parameters", "request_body")
63
64
 
64
65
 
65
66
  @dataclass
@@ -56,7 +56,7 @@ StrategyFactory = Callable[
56
56
 
57
57
  @st.composite # type: ignore
58
58
  def openapi_cases(
59
- draw: Callable,
59
+ draw: st.DrawFn,
60
60
  *,
61
61
  operation: APIOperation,
62
62
  hooks: HookDispatcher | None = None,
@@ -116,7 +116,7 @@ def openapi_cases(
116
116
  else:
117
117
  candidates = operation.body.items
118
118
  parameter = draw(st.sampled_from(candidates))
119
- strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
119
+ strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config, draw)
120
120
  strategy = apply_hooks(operation, ctx, hooks, strategy, ParameterLocation.BODY)
121
121
  # Parameter may have a wildcard media type. In this case, choose any supported one
122
122
  possible_media_types = sorted(
@@ -200,11 +200,15 @@ def openapi_cases(
200
200
  return instance
201
201
 
202
202
 
203
+ OPTIONAL_BODY_RATE = 0.05
204
+
205
+
203
206
  def _get_body_strategy(
204
207
  parameter: OpenApiBody,
205
208
  strategy_factory: StrategyFactory,
206
209
  operation: APIOperation,
207
210
  generation_config: GenerationConfig,
211
+ draw: st.DrawFn,
208
212
  ) -> st.SearchStrategy:
209
213
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
210
214
 
@@ -220,7 +224,12 @@ def _get_body_strategy(
220
224
  generation_config,
221
225
  operation.schema.adapter.jsonschema_validator_cls,
222
226
  )
223
- if not parameter.is_required:
227
+ # It is likely will be rejected, hence choose it rarely
228
+ if (
229
+ not parameter.is_required
230
+ and draw(st.floats(min_value=0.0, max_value=1.0, allow_infinity=False, allow_nan=False, allow_subnormal=False))
231
+ < OPTIONAL_BODY_RATE
232
+ ):
224
233
  strategy |= st.just(NOT_SET)
225
234
  return strategy
226
235
 
@@ -228,7 +237,7 @@ def _get_body_strategy(
228
237
  def get_parameters_value(
229
238
  value: dict[str, Any] | None,
230
239
  location: ParameterLocation,
231
- draw: Callable,
240
+ draw: st.DrawFn,
232
241
  operation: APIOperation,
233
242
  ctx: HookContext,
234
243
  hooks: HookDispatcher | None,
@@ -279,7 +288,7 @@ def generate_parameter(
279
288
  location: ParameterLocation,
280
289
  explicit: dict[str, Any] | None,
281
290
  operation: APIOperation,
282
- draw: Callable,
291
+ draw: st.DrawFn,
283
292
  ctx: HookContext,
284
293
  hooks: HookDispatcher | None,
285
294
  generator: GenerationMode,
@@ -5,8 +5,9 @@ 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
+ from schemathesis.core import NOT_SET
10
11
  from schemathesis.core.compat import RefResolutionError
11
12
  from schemathesis.core.result import Ok
12
13
  from schemathesis.specs.openapi.stateful.dependencies.inputs import extract_inputs, update_input_field_bindings
@@ -16,6 +17,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
16
17
  DefinitionSource,
17
18
  DependencyGraph,
18
19
  InputSlot,
20
+ NormalizedLink,
19
21
  OperationMap,
20
22
  OperationNode,
21
23
  OutputSlot,
@@ -25,6 +27,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
25
27
  from schemathesis.specs.openapi.stateful.dependencies.outputs import extract_outputs
26
28
 
27
29
  if TYPE_CHECKING:
30
+ from schemathesis.schemas import APIOperation
28
31
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
29
32
 
30
33
  __all__ = [
@@ -86,3 +89,122 @@ def analyze(schema: BaseOpenAPISchema) -> DependencyGraph:
86
89
  update_input_field_bindings(resource, operations)
87
90
 
88
91
  return DependencyGraph(operations=operations, resources=resources)
92
+
93
+
94
+ def inject_links(schema: BaseOpenAPISchema) -> int:
95
+ injected = 0
96
+ graph = analyze(schema)
97
+ for response_links in graph.iter_links():
98
+ operation = schema.get_operation_by_reference(response_links.producer_operation_ref)
99
+ response = operation.responses.get(response_links.status_code)
100
+ links = response.definition.setdefault(schema.adapter.links_keyword, {})
101
+
102
+ # Normalize existing links once
103
+ if links:
104
+ normalized_existing = [_normalize_link(link, schema) for link in links.values()]
105
+ else:
106
+ normalized_existing = []
107
+
108
+ for link_name, definition in response_links.links.items():
109
+ inferred_link = definition.to_openapi()
110
+
111
+ # Check if duplicate / subsets exists
112
+ if normalized_existing:
113
+ normalized = _normalize_link(inferred_link, schema)
114
+ if any(_is_subset_link(normalized, existing) for existing in normalized_existing):
115
+ continue
116
+
117
+ # Find unique name if collision exists
118
+ final_name = _resolve_link_name_collision(link_name, links)
119
+ links[final_name] = inferred_link
120
+ injected += 1
121
+ return injected
122
+
123
+
124
+ def _normalize_link(link: dict[str, Any], schema: BaseOpenAPISchema) -> NormalizedLink:
125
+ """Normalize a link definition for comparison."""
126
+ operation = _resolve_link_operation(link, schema)
127
+
128
+ normalized_params = _normalize_parameter_keys(link.get("parameters", {}), operation)
129
+
130
+ return NormalizedLink(
131
+ path=operation.path,
132
+ method=operation.method,
133
+ parameters=normalized_params,
134
+ request_body=link.get("requestBody", {}),
135
+ )
136
+
137
+
138
+ def _normalize_parameter_keys(parameters: dict, operation: APIOperation) -> set[str]:
139
+ """Normalize parameter keys to location.name format."""
140
+ normalized = set()
141
+
142
+ for parameter_name in parameters.keys():
143
+ # If already has location prefix, use as-is
144
+ if "." in parameter_name:
145
+ normalized.add(parameter_name)
146
+ continue
147
+
148
+ # Find the parameter and prepend location
149
+ for parameter in operation.iter_parameters():
150
+ if parameter.name == parameter_name:
151
+ normalized.add(f"{parameter.location.value}.{parameter_name}")
152
+ break
153
+
154
+ return normalized
155
+
156
+
157
+ def _resolve_link_operation(link: dict, schema: BaseOpenAPISchema) -> APIOperation:
158
+ """Resolve link to operation, handling both operationRef and operationId."""
159
+ if "operationRef" in link:
160
+ return schema.get_operation_by_reference(link["operationRef"])
161
+ return schema.get_operation_by_id(link["operationId"])
162
+
163
+
164
+ def _resolve_link_name_collision(proposed_name: str, existing_links: dict[str, Any]) -> str:
165
+ """Find unique link name if collision exists."""
166
+ if proposed_name not in existing_links:
167
+ return proposed_name
168
+
169
+ suffix = 0
170
+ while True:
171
+ candidate = f"{proposed_name}_{suffix}"
172
+ if candidate not in existing_links:
173
+ return candidate
174
+ suffix += 1
175
+
176
+
177
+ def _is_subset_link(inferred: NormalizedLink, existing: NormalizedLink) -> bool:
178
+ """Check if inferred link is a subset of existing link."""
179
+ # Must target the same operation
180
+ if inferred.path != existing.path or inferred.method != existing.method:
181
+ return False
182
+
183
+ # Inferred parameters must be subset of existing parameters
184
+ if not inferred.parameters.issubset(existing.parameters):
185
+ return False
186
+
187
+ # Inferred request body must be subset of existing body
188
+ return _is_request_body_subset(inferred.request_body, existing.request_body)
189
+
190
+
191
+ def _is_request_body_subset(inferred_body: Any, existing_body: Any) -> bool:
192
+ """Check if inferred body is a subset of existing body."""
193
+ # Empty inferred body is always a subset
194
+ if not inferred_body:
195
+ return True
196
+
197
+ # If existing is empty but inferred isn't, not a subset
198
+ if not existing_body:
199
+ return False
200
+
201
+ # Both must be dicts for subset comparison, otherwise check for equality
202
+ if not isinstance(inferred_body, dict) or not isinstance(existing_body, dict):
203
+ return inferred_body == existing_body
204
+
205
+ # Check if all inferred fields exist in existing with same values
206
+ for key, value in inferred_body.items():
207
+ if existing_body.get(key, NOT_SET) != value:
208
+ return False
209
+
210
+ return True
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Iterator
5
5
  from schemathesis.core import media_types
6
6
  from schemathesis.core.errors import MalformedMediaType
7
7
  from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
8
+ from schemathesis.core.jsonschema.types import get_type
8
9
  from schemathesis.core.parameters import ParameterLocation
9
10
  from schemathesis.specs.openapi.stateful.dependencies import naming
10
11
  from schemathesis.specs.openapi.stateful.dependencies.models import (
@@ -36,6 +37,7 @@ def extract_inputs(
36
37
  Connects each parameter (e.g., `userId`) to its resource definition (`User`),
37
38
  creating placeholder resources if not yet discovered from their schemas.
38
39
  """
40
+ known_dependencies = set()
39
41
  for param in operation.path_parameters:
40
42
  input_slot = _resolve_parameter_dependency(
41
43
  parameter_name=param.name,
@@ -47,12 +49,16 @@ def extract_inputs(
47
49
  canonicalization_cache=canonicalization_cache,
48
50
  )
49
51
  if input_slot is not None:
52
+ if input_slot.resource.source >= DefinitionSource.SCHEMA_WITH_PROPERTIES:
53
+ known_dependencies.add(input_slot.resource.name)
50
54
  yield input_slot
51
55
 
52
56
  for body in operation.body:
53
57
  try:
54
58
  if media_types.is_json(body.media_type):
55
- yield from _resolve_body_dependencies(body=body, operation=operation, resources=resources)
59
+ yield from _resolve_body_dependencies(
60
+ body=body, operation=operation, resources=resources, known_dependencies=known_dependencies
61
+ )
56
62
  except MalformedMediaType:
57
63
  continue
58
64
 
@@ -162,8 +168,23 @@ def _find_resource_in_responses(
162
168
  return None
163
169
 
164
170
 
171
+ GENERIC_FIELD_NAMES = frozenset(
172
+ {
173
+ "body",
174
+ "text",
175
+ "content",
176
+ "message",
177
+ "description",
178
+ }
179
+ )
180
+
181
+
165
182
  def _resolve_body_dependencies(
166
- *, body: OpenApiBody, operation: APIOperation, resources: ResourceMap
183
+ *,
184
+ body: OpenApiBody,
185
+ operation: APIOperation,
186
+ resources: ResourceMap,
187
+ known_dependencies: set[str],
167
188
  ) -> Iterator[InputSlot]:
168
189
  schema = body.raw_schema
169
190
  if not isinstance(schema, dict):
@@ -178,31 +199,57 @@ def _resolve_body_dependencies(
178
199
 
179
200
  # Inspect each property that could be a part of some other resource
180
201
  properties = resolved.get("properties", {})
202
+ required = resolved.get("required", [])
181
203
  path = operation.path
182
- for property_name in properties:
204
+ for property_name, subschema in properties.items():
183
205
  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,
206
+ if resource_name is not None:
207
+ resource = resources.get(resource_name)
208
+ if resource is None:
209
+ resource = ResourceDefinition.inferred_from_parameter(
210
+ name=resource_name,
211
+ parameter_name=property_name,
212
+ )
213
+ resources[resource_name] = resource
214
+ field = property_name
215
+ else:
216
+ field = (
217
+ naming.find_matching_field(
218
+ parameter=property_name,
219
+ resource=resource_name,
220
+ fields=resource.fields,
221
+ )
222
+ or "id"
200
223
  )
201
- or "id"
224
+ yield InputSlot(
225
+ resource=resource,
226
+ resource_field=field,
227
+ parameter_name=property_name,
228
+ parameter_location=ParameterLocation.BODY,
202
229
  )
230
+ continue
231
+
232
+ # Skip generic property names & optional fields (at least for now)
233
+ if property_name in GENERIC_FIELD_NAMES or property_name not in required:
234
+ continue
235
+
236
+ # Find candidate resources among known dependencies that actually have this field
237
+ candidates = [
238
+ resources[dep] for dep in known_dependencies if dep in resources and property_name in resources[dep].fields
239
+ ]
240
+
241
+ # Skip ambiguous cases when multiple resources have same field name
242
+ if len(candidates) != 1:
243
+ continue
244
+
245
+ resource = candidates[0]
246
+ # Ensure the target field supports the same type
247
+ if not resource.types[property_name] & set(get_type(subschema)):
248
+ continue
249
+
203
250
  yield InputSlot(
204
251
  resource=resource,
205
- resource_field=field,
252
+ resource_field=property_name,
206
253
  parameter_name=property_name,
207
254
  parameter_location=ParameterLocation.BODY,
208
255
  )
@@ -73,6 +73,11 @@ class DependencyGraph:
73
73
  parameters = {
74
74
  f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
75
75
  }
76
+ existing = links.get(link_name)
77
+ if existing is not None:
78
+ existing.parameters.update(parameters)
79
+ existing.request_body.update(request_body)
80
+ continue
76
81
  links[link_name] = LinkDefinition(
77
82
  operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
78
83
  parameters=parameters,
@@ -155,12 +160,13 @@ class LinkDefinition:
155
160
  """Convert to OpenAPI Links format."""
156
161
  links: dict[str, Any] = {
157
162
  "operationRef": self.operation_ref,
163
+ SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True},
158
164
  }
159
165
  if self.parameters:
160
166
  links["parameters"] = self.parameters
161
167
  if self.request_body:
162
168
  links["requestBody"] = self.request_body
163
- links[SCHEMATHESIS_LINK_EXTENSION] = {"merge_body": True}
169
+ links[SCHEMATHESIS_LINK_EXTENSION]["merge_body"] = True
164
170
  return links
165
171
 
166
172
 
@@ -195,6 +201,18 @@ class ResponseLinks:
195
201
  return {name: link_def.to_openapi() for name, link_def in self.links.items()}
196
202
 
197
203
 
204
+ @dataclass
205
+ class NormalizedLink:
206
+ """Normalized representation of a link."""
207
+
208
+ path: str
209
+ method: str
210
+ parameters: set[str]
211
+ request_body: Any
212
+
213
+ __slots__ = ("path", "method", "parameters", "request_body")
214
+
215
+
198
216
  class Cardinality(str, enum.Enum):
199
217
  """Whether there is one or many resources in a slot."""
200
218
 
@@ -254,18 +272,20 @@ class ResourceDefinition:
254
272
  name: str
255
273
  # A sorted list of resource fields
256
274
  fields: list[str]
275
+ # Field types mapping
276
+ types: dict[str, set[str]]
257
277
  # How this resource was created
258
278
  source: DefinitionSource
259
279
 
260
- __slots__ = ("name", "fields", "source")
280
+ __slots__ = ("name", "fields", "types", "source")
261
281
 
262
282
  @classmethod
263
283
  def without_properties(cls, name: str) -> ResourceDefinition:
264
- return cls(name=name, fields=[], source=DefinitionSource.SCHEMA_WITHOUT_PROPERTIES)
284
+ return cls(name=name, fields=[], types={}, source=DefinitionSource.SCHEMA_WITHOUT_PROPERTIES)
265
285
 
266
286
  @classmethod
267
287
  def inferred_from_parameter(cls, name: str, parameter_name: str) -> ResourceDefinition:
268
- return cls(name=name, fields=[parameter_name], source=DefinitionSource.PARAMETER_INFERENCE)
288
+ return cls(name=name, fields=[parameter_name], types={}, source=DefinitionSource.PARAMETER_INFERENCE)
269
289
 
270
290
 
271
291
  class DefinitionSource(enum.IntEnum):
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Iterator, Mapping, cast
5
5
 
6
6
  from schemathesis.core.errors import InfiniteRecursiveReference
7
7
  from schemathesis.core.jsonschema.bundler import BundleError
8
+ from schemathesis.core.jsonschema.types import get_type
8
9
  from schemathesis.specs.openapi.adapter.parameters import resource_name_from_ref
9
10
  from schemathesis.specs.openapi.adapter.references import maybe_resolve
10
11
  from schemathesis.specs.openapi.stateful.dependencies import naming
@@ -265,18 +266,21 @@ def _extract_resource_from_schema(
265
266
 
266
267
  properties = resolved.get("properties")
267
268
  if properties:
268
- fields = list(properties)
269
+ fields = sorted(properties)
270
+ types = {field: set(get_type(subschema)) for field, subschema in properties.items()}
269
271
  source = DefinitionSource.SCHEMA_WITH_PROPERTIES
270
272
  else:
271
273
  fields = []
274
+ types = {}
272
275
  source = DefinitionSource.SCHEMA_WITHOUT_PROPERTIES
273
276
  if resource is not None:
274
277
  if resource.source < source:
275
278
  resource.source = source
276
279
  resource.fields = fields
280
+ resource.types = types
277
281
  updated_resources.add(resource_name)
278
282
  else:
279
- resource = ResourceDefinition(name=resource_name, fields=fields, source=source)
283
+ resource = ResourceDefinition(name=resource_name, fields=fields, types=types, source=source)
280
284
  resources[resource_name] = resource
281
285
 
282
286
  return resource
@@ -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.3
3
+ Version: 4.3.5
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
@@ -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=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
@@ -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=jWrqEkEQPO2kgzxOffZacqxH6r7dkDmAx0ep9GA3NU8,64020
26
+ schemathesis/cli/commands/run/handlers/output.py,sha256=pPp5-lJP3Zir1sTA7fmlhc-u1Jn17enXZNUerQMr56M,64166
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
@@ -69,14 +69,14 @@ schemathesis/core/transport.py,sha256=LQcamAkFqJ0HuXQzepevAq2MCJW-uq5Nm-HE9yc7HM
69
69
  schemathesis/core/validation.py,sha256=b0USkKzkWvdz3jOW1JXYc_TfYshfKZeP7xAUnMqcNoc,2303
70
70
  schemathesis/core/version.py,sha256=dOBUWrY3-uA2NQXJp9z7EtZgkR6jYeLg8sMhQCL1mcI,205
71
71
  schemathesis/core/jsonschema/__init__.py,sha256=gBZGsXIpK2EFfcp8x0b69dqzWAm2OeZHepKImkkLvoE,320
72
- schemathesis/core/jsonschema/bundler.py,sha256=wK-UhI49TbfnNjr_riQZ28s005d4c-s05WcXLGQFlCs,7861
72
+ schemathesis/core/jsonschema/bundler.py,sha256=rHaNAVgBn0XvAk3t9dHsyym1xK4FyBW7zR1GLejPD0A,8204
73
73
  schemathesis/core/jsonschema/keywords.py,sha256=pjseXTfH9OItNs_Qq6ubkhNWQOrxTnwHmrP_jxrHeJU,631
74
74
  schemathesis/core/jsonschema/references.py,sha256=c2Q4IKWUbwENNtkbFaqf8r3LLZu6GFE5YLnYQlg5tPg,6069
75
75
  schemathesis/core/jsonschema/types.py,sha256=C7f9g8yKFuoxC5_0YNIh8QAyGU0-tj8pzTMfMDjjjVM,1248
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
@@ -99,13 +99,13 @@ schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz
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=tnDN_0MNT48mAO421JmcI3E0U2zeeTX-mjF3KXmpc7A,38629
102
+ schemathesis/generation/hypothesis/builder.py,sha256=j7R_X9Z_50xjwIl_Z8DVBe6P1r8leVNvQME7mRLXM1A,38520
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
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=DJjIxeTFpVfBXqZYUNnfDZSUXXt0ydQqOe75lWLmqlk,9098
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
@@ -129,7 +129,7 @@ schemathesis/specs/graphql/scalars.py,sha256=6lew8mnwhrtg23leiEbG43mLGPLlRln8mCl
129
129
  schemathesis/specs/graphql/schemas.py,sha256=GKJcnTAT1wUzzUr3r6wiTfiAdFLcgFQjYRRz7x4VQl0,14457
130
130
  schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzVfHxtdLkCPKM,1422
131
131
  schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
132
- schemathesis/specs/openapi/_hypothesis.py,sha256=g5476s_ArzheWKHHlOfKwx46tqoiehP3KaQM7L_AoEI,22359
132
+ schemathesis/specs/openapi/_hypothesis.py,sha256=O8vN-koBjzBVZfpD3pmgIt6ecU4ddAPHOxTAORd23Lo,22642
133
133
  schemathesis/specs/openapi/checks.py,sha256=YYV6j6idyw2ubY4sLp-avs2OVEkAWeIihjT0xiV1RRA,30669
134
134
  schemathesis/specs/openapi/converter.py,sha256=4a6-8STT5snF7B-t6IsOIGdK5rV16oNqsdvWL7VFf2M,6472
135
135
  schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
@@ -162,15 +162,15 @@ schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyD
162
162
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
163
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=DJDDCq73OYvCIPMxLKXJGTQGloNf6z6mgxjzjD0kJHA,8739
169
- schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=BkKSUK_irj-peBjQplvau-tyGbBKRJdKhzNkOTJ51l4,10650
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=0JM-FrY6Awv6gl-qDHaaK7pXbt_GKutBKPyIaph8apA,7842
168
+ schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=1qVVIlzx52qsy55Pht9dYNtn2dewRSiHegfrBO1RD8c,10347
169
+ schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=HxdVcVebjUFhSlSs_M8vDB-BnYfYwLGceIQAzytawrs,11324
170
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
- 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
172
+ schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=4bgXILFgC1_y9aU_4scaNw3lkJ6laW5MMkLYh3Ph4Hg,9894
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.3.dist-info/METADATA,sha256=VZo592TRmLn636zc-i_sq0FJh0d_zGmZoEZysc9bRe0,8540
183
- schemathesis-4.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
- schemathesis-4.3.3.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
- schemathesis-4.3.3.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
- schemathesis-4.3.3.dist-info/RECORD,,
182
+ schemathesis-4.3.5.dist-info/METADATA,sha256=f9Q8lbfSrfHg97P7fabHN6K0KN6qVpqFJjfg2B_w2QA,8540
183
+ schemathesis-4.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
+ schemathesis-4.3.5.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
+ schemathesis-4.3.5.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
+ schemathesis-4.3.5.dist-info/RECORD,,