schemathesis 3.13.0__py3-none-any.whl → 4.4.2__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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import enum
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from typing import Any, Iterator, Mapping
|
|
8
|
+
|
|
9
|
+
from typing_extensions import TypeAlias
|
|
10
|
+
|
|
11
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
12
|
+
from schemathesis.core.transforms import encode_pointer
|
|
13
|
+
from schemathesis.specs.openapi.stateful.links import SCHEMATHESIS_LINK_EXTENSION
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DependencyGraph:
|
|
18
|
+
"""Graph of API operations and their resource dependencies."""
|
|
19
|
+
|
|
20
|
+
operations: OperationMap
|
|
21
|
+
resources: ResourceMap
|
|
22
|
+
|
|
23
|
+
__slots__ = ("operations", "resources")
|
|
24
|
+
|
|
25
|
+
def serialize(self) -> dict[str, Any]:
|
|
26
|
+
serialized = asdict(self)
|
|
27
|
+
|
|
28
|
+
for operation in serialized["operations"].values():
|
|
29
|
+
del operation["method"]
|
|
30
|
+
del operation["path"]
|
|
31
|
+
for input in operation["inputs"]:
|
|
32
|
+
input["resource"] = input["resource"]["name"]
|
|
33
|
+
for output in operation["outputs"]:
|
|
34
|
+
output["resource"] = output["resource"]["name"]
|
|
35
|
+
|
|
36
|
+
for resource in serialized["resources"].values():
|
|
37
|
+
del resource["name"]
|
|
38
|
+
del resource["source"]
|
|
39
|
+
|
|
40
|
+
return serialized
|
|
41
|
+
|
|
42
|
+
def iter_links(self) -> Iterator[ResponseLinks]:
|
|
43
|
+
"""Generate OpenAPI Links connecting producer and consumer operations.
|
|
44
|
+
|
|
45
|
+
Creates links from operations that produce resources to operations that
|
|
46
|
+
consume them. For example: `POST /users` (creates `User`) -> `GET /users/{id}`
|
|
47
|
+
(needs `User.id` parameter).
|
|
48
|
+
"""
|
|
49
|
+
encoded_paths = {id(op): encode_pointer(op.path) for op in self.operations.values()}
|
|
50
|
+
|
|
51
|
+
# Index consumers by resource
|
|
52
|
+
consumers_by_resource: dict[int, dict[int, tuple[OperationNode, list[InputSlot]]]] = defaultdict(dict)
|
|
53
|
+
for consumer in self.operations.values():
|
|
54
|
+
consumer_id = id(consumer)
|
|
55
|
+
for input_slot in consumer.inputs:
|
|
56
|
+
resource_id = id(input_slot.resource)
|
|
57
|
+
if consumer_id not in consumers_by_resource[resource_id]:
|
|
58
|
+
consumers_by_resource[resource_id][consumer_id] = (consumer, [])
|
|
59
|
+
consumers_by_resource[resource_id][consumer_id][1].append(input_slot)
|
|
60
|
+
|
|
61
|
+
for producer in self.operations.values():
|
|
62
|
+
producer_path = encoded_paths[id(producer)]
|
|
63
|
+
producer_id = id(producer)
|
|
64
|
+
|
|
65
|
+
for output_slot in producer.outputs:
|
|
66
|
+
# Only iterate over consumers that match this resource
|
|
67
|
+
relevant_consumers = consumers_by_resource.get(id(output_slot.resource), {})
|
|
68
|
+
|
|
69
|
+
for consumer_id, (consumer, input_slots) in relevant_consumers.items():
|
|
70
|
+
# Skip self-references
|
|
71
|
+
if consumer_id == producer_id:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
consumer_path = encoded_paths[consumer_id]
|
|
75
|
+
links: dict[str, LinkDefinition] = {}
|
|
76
|
+
|
|
77
|
+
for input_slot in input_slots:
|
|
78
|
+
if input_slot.resource_field is not None:
|
|
79
|
+
body_pointer = extend_pointer(
|
|
80
|
+
output_slot.pointer, input_slot.resource_field, output_slot.cardinality
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
# No resource field means use the whole resource
|
|
84
|
+
body_pointer = output_slot.pointer
|
|
85
|
+
link_name = f"{consumer.method.capitalize()}{input_slot.resource.name}"
|
|
86
|
+
parameters = {}
|
|
87
|
+
request_body: dict[str, Any] | list = {}
|
|
88
|
+
# Data is extracted from response body
|
|
89
|
+
if input_slot.parameter_location == ParameterLocation.BODY:
|
|
90
|
+
if isinstance(input_slot.parameter_name, int):
|
|
91
|
+
request_body = [f"$response.body#{body_pointer}"]
|
|
92
|
+
else:
|
|
93
|
+
request_body = {
|
|
94
|
+
input_slot.parameter_name: f"$response.body#{body_pointer}",
|
|
95
|
+
}
|
|
96
|
+
else:
|
|
97
|
+
parameters = {
|
|
98
|
+
f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
|
|
99
|
+
}
|
|
100
|
+
existing = links.get(link_name)
|
|
101
|
+
if existing is not None:
|
|
102
|
+
existing.parameters.update(parameters)
|
|
103
|
+
if isinstance(existing.request_body, dict) and isinstance(request_body, dict):
|
|
104
|
+
existing.request_body.update(request_body)
|
|
105
|
+
else:
|
|
106
|
+
existing.request_body = request_body
|
|
107
|
+
continue
|
|
108
|
+
links[link_name] = LinkDefinition(
|
|
109
|
+
operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
|
|
110
|
+
parameters=parameters,
|
|
111
|
+
request_body=request_body,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if links:
|
|
115
|
+
yield ResponseLinks(
|
|
116
|
+
producer_operation_ref=f"#/paths/{producer_path}/{producer.method}",
|
|
117
|
+
status_code=output_slot.status_code,
|
|
118
|
+
links=links,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def assert_fieldless_resources(self, key: str, known: dict[str, frozenset[str]]) -> None: # pragma: no cover
|
|
122
|
+
"""Verify all resources have at least one field."""
|
|
123
|
+
# Fieldless resources usually indicate failed schema extraction, which can be caused by a bug
|
|
124
|
+
known_fieldless = known.get(key, frozenset())
|
|
125
|
+
|
|
126
|
+
for name, resource in self.resources.items():
|
|
127
|
+
if not resource.fields and name not in known_fieldless:
|
|
128
|
+
raise AssertionError(f"Resource {name} has no fields")
|
|
129
|
+
|
|
130
|
+
def assert_incorrect_field_mappings(self, key: str, known: dict[str, frozenset[str]]) -> None:
|
|
131
|
+
"""Verify all input slots reference valid fields in their resources."""
|
|
132
|
+
known_mismatches = known.get(key, frozenset())
|
|
133
|
+
|
|
134
|
+
for operation in self.operations.values():
|
|
135
|
+
for input in operation.inputs:
|
|
136
|
+
# Skip unreliable definition sources
|
|
137
|
+
if input.resource.source < DefinitionSource.SCHEMA_WITH_PROPERTIES:
|
|
138
|
+
continue
|
|
139
|
+
resource = self.resources[input.resource.name]
|
|
140
|
+
if (
|
|
141
|
+
input.resource_field not in resource.fields
|
|
142
|
+
and resource.name not in known_mismatches
|
|
143
|
+
and input.resource_field is not None
|
|
144
|
+
): # pragma: no cover
|
|
145
|
+
message = (
|
|
146
|
+
f"Operation '{operation.method.upper()} {operation.path}': "
|
|
147
|
+
f"InputSlot references field '{input.resource_field}' "
|
|
148
|
+
f"not found in resource '{resource.name}'"
|
|
149
|
+
)
|
|
150
|
+
matches = difflib.get_close_matches(input.resource_field, resource.fields, n=1, cutoff=0.6)
|
|
151
|
+
if matches:
|
|
152
|
+
message += f". Closest field - `{matches[0]}`"
|
|
153
|
+
if resource.fields:
|
|
154
|
+
message += f". Available fields - {', '.join(resource.fields)}"
|
|
155
|
+
else:
|
|
156
|
+
message += ". Resource has no fields"
|
|
157
|
+
raise AssertionError(message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def extend_pointer(base: str, field: str, cardinality: Cardinality) -> str:
|
|
161
|
+
if not base.endswith("/"):
|
|
162
|
+
base += "/"
|
|
163
|
+
if cardinality == Cardinality.MANY:
|
|
164
|
+
# For arrays, reference first element: /data → /data/0
|
|
165
|
+
base += "0/"
|
|
166
|
+
base += encode_pointer(field)
|
|
167
|
+
return base
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class LinkDefinition:
|
|
172
|
+
"""OpenAPI Link Object definition.
|
|
173
|
+
|
|
174
|
+
Represents a single link from a producer operation's response to a
|
|
175
|
+
consumer operation's input parameter.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
operation_ref: str
|
|
179
|
+
"""Reference to target operation (e.g., '#/paths/~1users~1{id}/get')"""
|
|
180
|
+
|
|
181
|
+
parameters: dict[str, str]
|
|
182
|
+
"""Parameter mappings (e.g., {'path.id': '$response.body#/id'})"""
|
|
183
|
+
|
|
184
|
+
request_body: dict[str, str] | list
|
|
185
|
+
"""Request body (e.g., {'path.id': '$response.body#/id'})"""
|
|
186
|
+
|
|
187
|
+
__slots__ = ("operation_ref", "parameters", "request_body")
|
|
188
|
+
|
|
189
|
+
def to_openapi(self) -> dict[str, Any]:
|
|
190
|
+
"""Convert to OpenAPI Links format."""
|
|
191
|
+
links: dict[str, Any] = {
|
|
192
|
+
"operationRef": self.operation_ref,
|
|
193
|
+
SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True},
|
|
194
|
+
}
|
|
195
|
+
if self.parameters:
|
|
196
|
+
links["parameters"] = self.parameters
|
|
197
|
+
if self.request_body:
|
|
198
|
+
links["requestBody"] = self.request_body
|
|
199
|
+
links[SCHEMATHESIS_LINK_EXTENSION]["merge_body"] = True
|
|
200
|
+
return links
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class ResponseLinks:
|
|
205
|
+
"""Collection of OpenAPI Links for a producer operation's response.
|
|
206
|
+
|
|
207
|
+
Represents all links from a single response (e.g., POST /users -> 201)
|
|
208
|
+
to consumer operations that can use the produced resource.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
POST /users -> 201 might have links to:
|
|
212
|
+
- GET /users/{id}
|
|
213
|
+
- PATCH /users/{id}
|
|
214
|
+
- DELETE /users/{id}
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
producer_operation_ref: str
|
|
219
|
+
"""Reference to producer operation (e.g., '#/paths/~1users/post')"""
|
|
220
|
+
|
|
221
|
+
status_code: str
|
|
222
|
+
"""Response status code (e.g., '201', '200', 'default')"""
|
|
223
|
+
|
|
224
|
+
links: dict[str, LinkDefinition]
|
|
225
|
+
"""Named links (e.g., {'GetUserById': LinkDefinition(...)})"""
|
|
226
|
+
|
|
227
|
+
__slots__ = ("producer_operation_ref", "status_code", "links")
|
|
228
|
+
|
|
229
|
+
def to_openapi(self) -> dict[str, Any]:
|
|
230
|
+
"""Convert to OpenAPI response links format."""
|
|
231
|
+
return {name: link_def.to_openapi() for name, link_def in self.links.items()}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class NormalizedLink:
|
|
236
|
+
"""Normalized representation of a link."""
|
|
237
|
+
|
|
238
|
+
path: str
|
|
239
|
+
method: str
|
|
240
|
+
parameters: set[str]
|
|
241
|
+
request_body: Any
|
|
242
|
+
|
|
243
|
+
__slots__ = ("path", "method", "parameters", "request_body")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Cardinality(str, enum.Enum):
|
|
247
|
+
"""Whether there is one or many resources in a slot."""
|
|
248
|
+
|
|
249
|
+
ONE = "ONE"
|
|
250
|
+
MANY = "MANY"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class OperationNode:
|
|
255
|
+
"""An API operation with its input/output dependencies."""
|
|
256
|
+
|
|
257
|
+
method: str
|
|
258
|
+
path: str
|
|
259
|
+
# What this operation NEEDS
|
|
260
|
+
inputs: list[InputSlot]
|
|
261
|
+
# What this operation PRODUCES
|
|
262
|
+
outputs: list[OutputSlot]
|
|
263
|
+
|
|
264
|
+
__slots__ = ("method", "path", "inputs", "outputs")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class InputSlot:
|
|
269
|
+
"""A required input for an operation."""
|
|
270
|
+
|
|
271
|
+
# Which resource is needed
|
|
272
|
+
resource: ResourceDefinition
|
|
273
|
+
# Which field from that resource (e.g., "id").
|
|
274
|
+
# None if passing the whole resource
|
|
275
|
+
resource_field: str | None
|
|
276
|
+
# Where it goes in the request (e.g., "userId")
|
|
277
|
+
# Integer means index in an array (only single items are supported)
|
|
278
|
+
parameter_name: str | int
|
|
279
|
+
parameter_location: ParameterLocation
|
|
280
|
+
|
|
281
|
+
__slots__ = ("resource", "resource_field", "parameter_name", "parameter_location")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass
|
|
285
|
+
class OutputSlot:
|
|
286
|
+
"""Describes how to extract a resource from an operation's response."""
|
|
287
|
+
|
|
288
|
+
# Which resource type
|
|
289
|
+
resource: ResourceDefinition
|
|
290
|
+
# Where in response body (JSON pointer)
|
|
291
|
+
pointer: str
|
|
292
|
+
# Is this a single resource or an array?
|
|
293
|
+
cardinality: Cardinality
|
|
294
|
+
# HTTP status code
|
|
295
|
+
status_code: str
|
|
296
|
+
|
|
297
|
+
__slots__ = ("resource", "pointer", "cardinality", "status_code")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class ResourceDefinition:
|
|
302
|
+
"""A minimal description of a resource structure."""
|
|
303
|
+
|
|
304
|
+
name: str
|
|
305
|
+
# A sorted list of resource fields
|
|
306
|
+
fields: list[str]
|
|
307
|
+
# Field types mapping
|
|
308
|
+
types: dict[str, set[str]]
|
|
309
|
+
# How this resource was created
|
|
310
|
+
source: DefinitionSource
|
|
311
|
+
|
|
312
|
+
__slots__ = ("name", "fields", "types", "source")
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def without_properties(cls, name: str) -> ResourceDefinition:
|
|
316
|
+
return cls(name=name, fields=[], types={}, source=DefinitionSource.SCHEMA_WITHOUT_PROPERTIES)
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def inferred_from_parameter(cls, name: str, parameter_name: str | None) -> ResourceDefinition:
|
|
320
|
+
fields = [parameter_name] if parameter_name is not None else []
|
|
321
|
+
return cls(name=name, fields=fields, types={}, source=DefinitionSource.PARAMETER_INFERENCE)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class DefinitionSource(enum.IntEnum):
|
|
325
|
+
"""Quality level of resource information.
|
|
326
|
+
|
|
327
|
+
Lower values are less reliable and should be replaced by higher values.
|
|
328
|
+
Same values should be merged (union of fields).
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
# From spec but no structural information
|
|
332
|
+
SCHEMA_WITHOUT_PROPERTIES = 0
|
|
333
|
+
# Guessed from parameter names (not in spec)
|
|
334
|
+
PARAMETER_INFERENCE = 1
|
|
335
|
+
# From spec with actual field definitions
|
|
336
|
+
SCHEMA_WITH_PROPERTIES = 2
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
OperationMap: TypeAlias = dict[str, OperationNode]
|
|
340
|
+
ResourceMap: TypeAlias = dict[str, ResourceDefinition]
|
|
341
|
+
CanonicalizationCache: TypeAlias = dict[str, Mapping[str, Any]]
|