schemathesis 4.0.0a1__py3-none-any.whl → 4.0.0a2__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/checks.py +6 -4
- schemathesis/cli/commands/run/__init__.py +4 -4
- schemathesis/cli/commands/run/events.py +4 -9
- schemathesis/cli/commands/run/executor.py +6 -3
- schemathesis/cli/commands/run/filters.py +27 -19
- schemathesis/cli/commands/run/handlers/base.py +1 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +1 -3
- schemathesis/cli/commands/run/handlers/output.py +765 -143
- schemathesis/cli/commands/run/validation.py +1 -1
- schemathesis/cli/ext/options.py +4 -1
- schemathesis/core/failures.py +54 -24
- schemathesis/engine/core.py +1 -1
- schemathesis/engine/events.py +3 -97
- schemathesis/engine/phases/stateful/__init__.py +1 -0
- schemathesis/engine/phases/stateful/_executor.py +19 -44
- schemathesis/engine/phases/unit/__init__.py +1 -0
- schemathesis/engine/phases/unit/_executor.py +2 -1
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/engine/recorder.py +8 -3
- schemathesis/generation/stateful/state_machine.py +53 -36
- schemathesis/graphql/checks.py +3 -9
- schemathesis/openapi/checks.py +8 -33
- schemathesis/schemas.py +34 -14
- schemathesis/specs/graphql/schemas.py +16 -15
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/nodes.py +20 -20
- schemathesis/specs/openapi/links.py +126 -119
- schemathesis/specs/openapi/schemas.py +18 -22
- schemathesis/specs/openapi/stateful/__init__.py +77 -55
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/RECORD +34 -35
- schemathesis/specs/openapi/expressions/context.py +0 -14
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -9,10 +9,10 @@ from typing import TYPE_CHECKING, Any, cast
|
|
9
9
|
from requests.structures import CaseInsensitiveDict
|
10
10
|
|
11
11
|
from schemathesis.core.transforms import UNRESOLVABLE, resolve_pointer
|
12
|
+
from schemathesis.generation.stateful.state_machine import StepOutput
|
12
13
|
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
13
14
|
|
14
15
|
if TYPE_CHECKING:
|
15
|
-
from .context import ExpressionContext
|
16
16
|
from .extractors import Extractor
|
17
17
|
|
18
18
|
|
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|
20
20
|
class Node:
|
21
21
|
"""Generic expression node."""
|
22
22
|
|
23
|
-
def evaluate(self,
|
23
|
+
def evaluate(self, output: StepOutput) -> str:
|
24
24
|
raise NotImplementedError
|
25
25
|
|
26
26
|
|
@@ -39,7 +39,7 @@ class String(Node):
|
|
39
39
|
|
40
40
|
value: str
|
41
41
|
|
42
|
-
def evaluate(self,
|
42
|
+
def evaluate(self, output: StepOutput) -> str:
|
43
43
|
"""String tokens are passed as they are.
|
44
44
|
|
45
45
|
``foo{$request.path.id}``
|
@@ -53,11 +53,11 @@ class String(Node):
|
|
53
53
|
class URL(Node):
|
54
54
|
"""A node for `$url` expression."""
|
55
55
|
|
56
|
-
def evaluate(self,
|
56
|
+
def evaluate(self, output: StepOutput) -> str:
|
57
57
|
import requests
|
58
58
|
|
59
|
-
base_url =
|
60
|
-
kwargs = REQUESTS_TRANSPORT.serialize_case(
|
59
|
+
base_url = output.case.operation.base_url or "http://127.0.0.1"
|
60
|
+
kwargs = REQUESTS_TRANSPORT.serialize_case(output.case, base_url=base_url)
|
61
61
|
prepared = requests.Request(**kwargs).prepare()
|
62
62
|
return cast(str, prepared.url)
|
63
63
|
|
@@ -66,16 +66,16 @@ class URL(Node):
|
|
66
66
|
class Method(Node):
|
67
67
|
"""A node for `$method` expression."""
|
68
68
|
|
69
|
-
def evaluate(self,
|
70
|
-
return
|
69
|
+
def evaluate(self, output: StepOutput) -> str:
|
70
|
+
return output.case.operation.method.upper()
|
71
71
|
|
72
72
|
|
73
73
|
@dataclass
|
74
74
|
class StatusCode(Node):
|
75
75
|
"""A node for `$statusCode` expression."""
|
76
76
|
|
77
|
-
def evaluate(self,
|
78
|
-
return str(
|
77
|
+
def evaluate(self, output: StepOutput) -> str:
|
78
|
+
return str(output.response.status_code)
|
79
79
|
|
80
80
|
|
81
81
|
@dataclass
|
@@ -86,11 +86,11 @@ class NonBodyRequest(Node):
|
|
86
86
|
parameter: str
|
87
87
|
extractor: Extractor | None = None
|
88
88
|
|
89
|
-
def evaluate(self,
|
89
|
+
def evaluate(self, output: StepOutput) -> str:
|
90
90
|
container: dict | CaseInsensitiveDict = {
|
91
|
-
"query":
|
92
|
-
"path":
|
93
|
-
"header":
|
91
|
+
"query": output.case.query,
|
92
|
+
"path": output.case.path_parameters,
|
93
|
+
"header": output.case.headers,
|
94
94
|
}[self.location] or {}
|
95
95
|
if self.location == "header":
|
96
96
|
container = CaseInsensitiveDict(container)
|
@@ -108,8 +108,8 @@ class BodyRequest(Node):
|
|
108
108
|
|
109
109
|
pointer: str | None = None
|
110
110
|
|
111
|
-
def evaluate(self,
|
112
|
-
document =
|
111
|
+
def evaluate(self, output: StepOutput) -> Any:
|
112
|
+
document = output.case.body
|
113
113
|
if self.pointer is None:
|
114
114
|
return document
|
115
115
|
resolved = resolve_pointer(document, self.pointer[1:])
|
@@ -125,8 +125,8 @@ class HeaderResponse(Node):
|
|
125
125
|
parameter: str
|
126
126
|
extractor: Extractor | None = None
|
127
127
|
|
128
|
-
def evaluate(self,
|
129
|
-
value =
|
128
|
+
def evaluate(self, output: StepOutput) -> str:
|
129
|
+
value = output.response.headers.get(self.parameter.lower())
|
130
130
|
if value is None:
|
131
131
|
return ""
|
132
132
|
if self.extractor is not None:
|
@@ -140,8 +140,8 @@ class BodyResponse(Node):
|
|
140
140
|
|
141
141
|
pointer: str | None = None
|
142
142
|
|
143
|
-
def evaluate(self,
|
144
|
-
document =
|
143
|
+
def evaluate(self, output: StepOutput) -> Any:
|
144
|
+
document = output.response.json()
|
145
145
|
if self.pointer is None:
|
146
146
|
# We need the parsed document - data will be serialized before sending to the application
|
147
147
|
return document
|
@@ -1,17 +1,12 @@
|
|
1
|
-
"""Open API links support.
|
2
|
-
|
3
|
-
Based on https://swagger.io/docs/specification/links/
|
4
|
-
"""
|
5
|
-
|
6
1
|
from __future__ import annotations
|
7
2
|
|
8
|
-
from dataclasses import dataclass
|
9
|
-
from
|
10
|
-
from typing import TYPE_CHECKING, Any, Generator, Literal,
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from functools import lru_cache
|
5
|
+
from typing import TYPE_CHECKING, Any, Generator, Literal, Union, cast
|
11
6
|
|
12
7
|
from schemathesis.core import NOT_SET, NotSet
|
13
|
-
from schemathesis.
|
14
|
-
from schemathesis.generation.stateful.state_machine import
|
8
|
+
from schemathesis.core.result import Err, Ok, Result
|
9
|
+
from schemathesis.generation.stateful.state_machine import ExtractedParam, StepOutput, Transition
|
15
10
|
from schemathesis.schemas import APIOperation
|
16
11
|
|
17
12
|
from . import expressions
|
@@ -23,129 +18,141 @@ if TYPE_CHECKING:
|
|
23
18
|
|
24
19
|
|
25
20
|
SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
|
21
|
+
ParameterLocation = Literal["path", "query", "header", "cookie", "body"]
|
26
22
|
|
27
23
|
|
28
|
-
|
29
|
-
|
24
|
+
@dataclass
|
25
|
+
class NormalizedParameter:
|
26
|
+
"""Processed link parameter with resolved container information."""
|
27
|
+
|
28
|
+
location: ParameterLocation | None
|
29
|
+
name: str
|
30
|
+
expression: str
|
31
|
+
container_name: str
|
30
32
|
|
33
|
+
__slots__ = ("location", "name", "expression", "container_name")
|
31
34
|
|
32
|
-
@dataclass(repr=False)
|
33
|
-
class OpenAPILink(Direction):
|
34
|
-
"""Alternative approach to link processing.
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
@dataclass(repr=False)
|
37
|
+
class OpenApiLink:
|
38
|
+
"""Represents an OpenAPI link between operations."""
|
38
39
|
|
39
40
|
name: str
|
40
41
|
status_code: str
|
41
|
-
|
42
|
-
|
43
|
-
parameters: list[
|
44
|
-
body: dict[str, Any] | NotSet
|
45
|
-
merge_body: bool
|
46
|
-
|
47
|
-
def __repr__(self) -> str:
|
48
|
-
path = self.operation.path
|
49
|
-
method = self.operation.method
|
50
|
-
return f"state.schema['{path}']['{method}'].links['{self.status_code}']['{self.name}']"
|
51
|
-
|
52
|
-
def __post_init__(self) -> None:
|
53
|
-
extension = self.definition.get(SCHEMATHESIS_LINK_EXTENSION)
|
54
|
-
self.parameters = [
|
55
|
-
normalize_parameter(parameter, expression)
|
56
|
-
for parameter, expression in self.definition.get("parameters", {}).items()
|
57
|
-
]
|
58
|
-
self.body = self.definition.get("requestBody", NOT_SET)
|
59
|
-
if extension is not None:
|
60
|
-
self.merge_body = extension.get("merge_body", True)
|
61
|
-
|
62
|
-
def set_data(self, case: Case, **kwargs: Any) -> None:
|
63
|
-
"""Assign all linked definitions to the new case instance."""
|
64
|
-
context = kwargs["context"]
|
65
|
-
self.set_parameters(case, context)
|
66
|
-
self.set_body(case, context)
|
67
|
-
|
68
|
-
def set_parameters(self, case: Case, context: expressions.ExpressionContext) -> None:
|
69
|
-
for location, name, expression in self.parameters:
|
70
|
-
location, container = get_container(case, location, name)
|
71
|
-
# Might happen if there is directly specified container,
|
72
|
-
# but the schema has no parameters of such type at all.
|
73
|
-
# Therefore the container is empty, otherwise it will be at least an empty object
|
74
|
-
if container is None:
|
75
|
-
message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
|
76
|
-
possibilities = [param.name for param in case.operation.iter_parameters()]
|
77
|
-
matches = get_close_matches(name, possibilities)
|
78
|
-
if matches:
|
79
|
-
message += f" Did you mean `{matches[0]}`?"
|
80
|
-
raise ValueError(message)
|
81
|
-
value = expressions.evaluate(expression, context)
|
82
|
-
if value is not None:
|
83
|
-
container[name] = value
|
84
|
-
|
85
|
-
def set_body(
|
86
|
-
self,
|
87
|
-
case: Case,
|
88
|
-
context: expressions.ExpressionContext,
|
89
|
-
) -> None:
|
90
|
-
if self.body is not NOT_SET:
|
91
|
-
evaluated = expressions.evaluate(self.body, context, evaluate_nested=True)
|
92
|
-
if self.merge_body:
|
93
|
-
case.body = merge_body(case.body, evaluated)
|
94
|
-
else:
|
95
|
-
case.body = evaluated
|
96
|
-
|
97
|
-
def get_target_operation(self) -> APIOperation:
|
98
|
-
if "operationId" in self.definition:
|
99
|
-
return self.operation.schema.get_operation_by_id(self.definition["operationId"]) # type: ignore
|
100
|
-
return self.operation.schema.get_operation_by_reference(self.definition["operationRef"]) # type: ignore
|
101
|
-
|
102
|
-
|
103
|
-
def merge_body(old: Any, new: Any) -> Any:
|
104
|
-
if isinstance(old, dict) and isinstance(new, dict):
|
105
|
-
return {**old, **new}
|
106
|
-
return new
|
107
|
-
|
108
|
-
|
109
|
-
def get_container(
|
110
|
-
case: Case, location: Literal["path", "query", "header", "cookie", "body"] | None, name: str
|
111
|
-
) -> tuple[Literal["path", "query", "header", "cookie", "body"], dict[str, Any] | None]:
|
112
|
-
"""Get a container that suppose to store the given parameter."""
|
113
|
-
if location:
|
114
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
115
|
-
else:
|
116
|
-
for param in case.operation.iter_parameters():
|
117
|
-
if param.name == name:
|
118
|
-
location = param.location
|
119
|
-
container_name = LOCATION_TO_CONTAINER[param.location]
|
120
|
-
break
|
121
|
-
else:
|
122
|
-
raise ValueError(f"Parameter `{name}` is not defined in API operation `{case.operation.label}`")
|
123
|
-
return location, getattr(case, container_name)
|
124
|
-
|
125
|
-
|
126
|
-
def normalize_parameter(
|
127
|
-
parameter: str, expression: str
|
128
|
-
) -> tuple[Literal["path", "query", "header", "cookie", "body"] | None, str, str]:
|
129
|
-
"""Normalize runtime expressions.
|
42
|
+
source: APIOperation
|
43
|
+
target: APIOperation
|
44
|
+
parameters: list[NormalizedParameter]
|
45
|
+
body: dict[str, Any] | NotSet
|
46
|
+
merge_body: bool
|
130
47
|
|
131
|
-
|
132
|
-
At the same time, parameters could be defined without a prefix - `id`.
|
133
|
-
We need to normalize all parameters to the same form to simplify working with them.
|
134
|
-
"""
|
135
|
-
try:
|
136
|
-
# The parameter name is prefixed with its location. Example: `path.id`
|
137
|
-
location, name = tuple(parameter.split("."))
|
138
|
-
_location = cast(Literal["path", "query", "header", "cookie", "body"], location)
|
139
|
-
return _location, name, expression
|
140
|
-
except ValueError:
|
141
|
-
return None, parameter, expression
|
48
|
+
__slots__ = ("name", "status_code", "source", "target", "parameters", "body", "merge_body", "_cached_extract")
|
142
49
|
|
50
|
+
def __init__(self, name: str, status_code: str, definition: dict[str, Any], source: APIOperation):
|
51
|
+
self.name = name
|
52
|
+
self.status_code = status_code
|
53
|
+
self.source = source
|
143
54
|
|
144
|
-
|
55
|
+
if "operationId" in definition:
|
56
|
+
self.target = source.schema.get_operation_by_id(definition["operationId"]) # type: ignore
|
57
|
+
else:
|
58
|
+
self.target = source.schema.get_operation_by_reference(definition["operationRef"]) # type: ignore
|
59
|
+
|
60
|
+
extension = definition.get(SCHEMATHESIS_LINK_EXTENSION)
|
61
|
+
self.parameters = self._normalize_parameters(definition.get("parameters", {}))
|
62
|
+
self.body = definition.get("requestBody", NOT_SET)
|
63
|
+
self.merge_body = extension.get("merge_body", True) if extension else True
|
64
|
+
|
65
|
+
self._cached_extract = lru_cache(8)(self._extract_impl)
|
66
|
+
|
67
|
+
def _normalize_parameters(self, parameters: dict[str, str]) -> list[NormalizedParameter]:
|
68
|
+
"""Process link parameters and resolve their container locations.
|
69
|
+
|
70
|
+
Handles both explicit locations (e.g., "path.id") and implicit ones resolved from target operation.
|
71
|
+
"""
|
72
|
+
result = []
|
73
|
+
for parameter, expression in parameters.items():
|
74
|
+
location: ParameterLocation | None
|
75
|
+
try:
|
76
|
+
# The parameter name is prefixed with its location. Example: `path.id`
|
77
|
+
_location, name = tuple(parameter.split("."))
|
78
|
+
location = cast(ParameterLocation, _location)
|
79
|
+
except ValueError:
|
80
|
+
location = None
|
81
|
+
name = parameter
|
82
|
+
|
83
|
+
container_name = self._get_parameter_container(location, name)
|
84
|
+
result.append(NormalizedParameter(location, name, expression, container_name))
|
85
|
+
return result
|
86
|
+
|
87
|
+
def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
|
88
|
+
"""Resolve parameter container either from explicit location or by looking up in target operation."""
|
89
|
+
if location:
|
90
|
+
return LOCATION_TO_CONTAINER[location]
|
91
|
+
|
92
|
+
for param in self.target.iter_parameters():
|
93
|
+
if param.name == name:
|
94
|
+
return LOCATION_TO_CONTAINER[param.location]
|
95
|
+
raise ValueError(f"Parameter `{name}` is not defined in API operation `{self.target.label}`")
|
96
|
+
|
97
|
+
def extract(self, output: StepOutput) -> Transition:
|
98
|
+
return self._cached_extract(StepOutputWrapper(output))
|
99
|
+
|
100
|
+
def _extract_impl(self, wrapper: StepOutputWrapper) -> Transition:
|
101
|
+
output = wrapper.output
|
102
|
+
return Transition(
|
103
|
+
id=f"{self.source.label} - {self.status_code} - {self.name}",
|
104
|
+
parent_id=output.case.id,
|
105
|
+
parameters=self.extract_parameters(output),
|
106
|
+
request_body=self.extract_body(output),
|
107
|
+
)
|
108
|
+
|
109
|
+
def extract_parameters(self, output: StepOutput) -> dict[str, dict[str, ExtractedParam]]:
|
110
|
+
"""Extract parameters using runtime expressions.
|
111
|
+
|
112
|
+
Returns a two-level dictionary: container -> parameter name -> extracted value
|
113
|
+
"""
|
114
|
+
extracted: dict[str, dict[str, ExtractedParam]] = {}
|
115
|
+
for parameter in self.parameters:
|
116
|
+
container = extracted.setdefault(parameter.container_name, {})
|
117
|
+
value: Result[Any, Exception]
|
118
|
+
try:
|
119
|
+
value = Ok(expressions.evaluate(parameter.expression, output))
|
120
|
+
except Exception as exc:
|
121
|
+
value = Err(exc)
|
122
|
+
container[parameter.name] = ExtractedParam(definition=parameter.expression, value=value)
|
123
|
+
return extracted
|
124
|
+
|
125
|
+
def extract_body(self, output: StepOutput) -> ExtractedParam | None:
|
126
|
+
if not isinstance(self.body, NotSet):
|
127
|
+
value: Result[Any, Exception]
|
128
|
+
try:
|
129
|
+
value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
|
130
|
+
except Exception as exc:
|
131
|
+
value = Err(exc)
|
132
|
+
return ExtractedParam(definition=self.body, value=value)
|
133
|
+
return None
|
134
|
+
|
135
|
+
|
136
|
+
@dataclass
|
137
|
+
class StepOutputWrapper:
|
138
|
+
"""Wrapper for StepOutput that uses only case_id for hash-based caching."""
|
139
|
+
|
140
|
+
output: StepOutput
|
141
|
+
__slots__ = ("output",)
|
142
|
+
|
143
|
+
def __hash__(self) -> int:
|
144
|
+
return hash(self.output.case.id)
|
145
|
+
|
146
|
+
def __eq__(self, other: object) -> bool:
|
147
|
+
assert isinstance(other, StepOutputWrapper)
|
148
|
+
return self.output.case.id == other.output.case.id
|
149
|
+
|
150
|
+
|
151
|
+
def get_all_links(operation: APIOperation) -> Generator[tuple[str, OpenApiLink], None, None]:
|
145
152
|
for status_code, definition in operation.definition.raw["responses"].items():
|
146
153
|
definition = operation.schema.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8) # type: ignore[attr-defined]
|
147
154
|
for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
|
148
|
-
yield status_code,
|
155
|
+
yield status_code, OpenApiLink(name, status_code, link_definition, operation)
|
149
156
|
|
150
157
|
|
151
158
|
StatusCode = Union[str, int]
|
@@ -44,7 +44,7 @@ from schemathesis.openapi.checks import JsonSchemaError, MissingContentType
|
|
44
44
|
|
45
45
|
from ...generation import GenerationConfig, GenerationMode
|
46
46
|
from ...hooks import HookContext, HookDispatcher
|
47
|
-
from ...schemas import APIOperation, APIOperationMap,
|
47
|
+
from ...schemas import APIOperation, APIOperationMap, ApiStatistic, BaseSchema, OperationDefinition
|
48
48
|
from . import links, serialization
|
49
49
|
from ._cache import OperationCache
|
50
50
|
from ._hypothesis import openapi_cases
|
@@ -166,15 +166,16 @@ class BaseOpenAPISchema(BaseSchema):
|
|
166
166
|
operation.schema = self
|
167
167
|
return not self.filter_set.match(_ctx_cache)
|
168
168
|
|
169
|
-
def
|
170
|
-
|
169
|
+
def _measure_statistic(self) -> ApiStatistic:
|
170
|
+
statistic = ApiStatistic()
|
171
171
|
try:
|
172
172
|
paths = self.raw_schema["paths"]
|
173
173
|
except KeyError:
|
174
|
-
return
|
174
|
+
return statistic
|
175
175
|
|
176
176
|
resolve = self.resolver.resolve
|
177
177
|
should_skip = self._should_skip
|
178
|
+
links_field = self.links_field
|
178
179
|
|
179
180
|
for path, path_item in paths.items():
|
180
181
|
try:
|
@@ -183,12 +184,21 @@ class BaseOpenAPISchema(BaseSchema):
|
|
183
184
|
for method, definition in path_item.items():
|
184
185
|
if method not in HTTP_METHODS:
|
185
186
|
continue
|
186
|
-
|
187
|
-
|
188
|
-
|
187
|
+
statistic.operations.total += 1
|
188
|
+
is_selected = not should_skip(path, method, definition)
|
189
|
+
if is_selected:
|
190
|
+
statistic.operations.selected += 1
|
191
|
+
for response in definition.get("responses", {}).values():
|
192
|
+
if "$ref" in response:
|
193
|
+
_, response = resolve(response["$ref"])
|
194
|
+
defined_links = response.get(links_field)
|
195
|
+
if defined_links is not None:
|
196
|
+
statistic.links.total += len(defined_links)
|
197
|
+
if is_selected:
|
198
|
+
statistic.links.selected = len(defined_links)
|
189
199
|
except SCHEMA_PARSING_ERRORS:
|
190
200
|
continue
|
191
|
-
return
|
201
|
+
return statistic
|
192
202
|
|
193
203
|
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
194
204
|
try:
|
@@ -210,20 +220,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
210
220
|
# Ignore errors
|
211
221
|
continue
|
212
222
|
|
213
|
-
@property
|
214
|
-
def links_count(self) -> int:
|
215
|
-
total = 0
|
216
|
-
resolve = self.resolver.resolve
|
217
|
-
links_field = self.links_field
|
218
|
-
for definition in self._operation_iter():
|
219
|
-
for response in definition.get("responses", {}).values():
|
220
|
-
if "$ref" in response:
|
221
|
-
_, response = resolve(response["$ref"])
|
222
|
-
defined_links = response.get(links_field)
|
223
|
-
if defined_links is not None:
|
224
|
-
total += len(defined_links)
|
225
|
-
return total
|
226
|
-
|
227
223
|
def override(
|
228
224
|
self,
|
229
225
|
*,
|