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,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Iterator, cast
|
|
7
|
+
|
|
8
|
+
from schemathesis.core.failures import Failure
|
|
9
|
+
from schemathesis.core.transport import Response
|
|
10
|
+
from schemathesis.engine import Status
|
|
11
|
+
from schemathesis.generation.case import Case
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from schemathesis.generation.stateful.state_machine import Transition
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ScenarioRecorder:
|
|
21
|
+
"""Tracks and organizes all data related to a logical block of testing.
|
|
22
|
+
|
|
23
|
+
Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Human-readable label
|
|
27
|
+
label: str
|
|
28
|
+
|
|
29
|
+
# Recorded test cases
|
|
30
|
+
cases: dict[str, CaseNode]
|
|
31
|
+
# Results of checks categorized by test case ID
|
|
32
|
+
checks: dict[str, list[CheckNode]]
|
|
33
|
+
# Network interactions by test case ID
|
|
34
|
+
interactions: dict[str, Interaction]
|
|
35
|
+
__slots__ = ("label", "status", "roots", "cases", "checks", "interactions")
|
|
36
|
+
|
|
37
|
+
def __init__(self, *, label: str) -> None:
|
|
38
|
+
self.label = label
|
|
39
|
+
self.cases = {}
|
|
40
|
+
self.checks = {}
|
|
41
|
+
self.interactions = {}
|
|
42
|
+
|
|
43
|
+
def record_case(
|
|
44
|
+
self, *, parent_id: str | None, case: Case, transition: Transition | None, is_transition_applied: bool
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Record a test case and its relationship to a parent, if applicable."""
|
|
47
|
+
self.cases[case.id] = CaseNode(
|
|
48
|
+
value=case,
|
|
49
|
+
parent_id=parent_id,
|
|
50
|
+
transition=transition,
|
|
51
|
+
is_transition_applied=is_transition_applied,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def record_response(self, *, case_id: str, response: Response) -> None:
|
|
55
|
+
"""Record the API response for a given test case."""
|
|
56
|
+
request = Request.from_prepared_request(response.request)
|
|
57
|
+
self.interactions[case_id] = Interaction(request=request, response=response)
|
|
58
|
+
|
|
59
|
+
def record_request(self, *, case_id: str, request: requests.PreparedRequest) -> None:
|
|
60
|
+
"""Record a network-level error for a given test case."""
|
|
61
|
+
self.interactions[case_id] = Interaction(request=Request.from_prepared_request(request), response=None)
|
|
62
|
+
|
|
63
|
+
def record_check_failure(self, *, name: str, case_id: str, code_sample: str, failure: Failure) -> None:
|
|
64
|
+
"""Record a failure of a check for a given test case."""
|
|
65
|
+
self.checks.setdefault(case_id, []).append(
|
|
66
|
+
CheckNode(
|
|
67
|
+
name=name,
|
|
68
|
+
status=Status.FAILURE,
|
|
69
|
+
failure_info=CheckFailureInfo(code_sample=code_sample, failure=failure),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def record_check_success(self, *, name: str, case_id: str) -> None:
|
|
74
|
+
"""Record a successful pass of a check for a given test case."""
|
|
75
|
+
self.checks.setdefault(case_id, []).append(CheckNode(name=name, status=Status.SUCCESS, failure_info=None))
|
|
76
|
+
|
|
77
|
+
def find_failure_data(self, *, parent_id: str, failure: Failure) -> FailureData:
|
|
78
|
+
"""Retrieve the relevant test case & interaction data for a failure.
|
|
79
|
+
|
|
80
|
+
It may happen that a failure comes from a different test case if a check generated some additional
|
|
81
|
+
test cases & interactions.
|
|
82
|
+
"""
|
|
83
|
+
case_id = failure.case_id or parent_id
|
|
84
|
+
case = self.cases[case_id].value
|
|
85
|
+
request = self.interactions[case_id].request
|
|
86
|
+
response = self.interactions[case_id].response
|
|
87
|
+
assert isinstance(response, Response)
|
|
88
|
+
headers = {key: value[0] for key, value in request.headers.items()}
|
|
89
|
+
return FailureData(case=case, headers=headers, verify=response.verify)
|
|
90
|
+
|
|
91
|
+
def find_parent(self, *, case_id: str) -> Case | None:
|
|
92
|
+
"""Find the parent case of a given test case, if it exists."""
|
|
93
|
+
case = self.cases.get(case_id)
|
|
94
|
+
if case is not None and case.parent_id is not None:
|
|
95
|
+
parent = self.cases.get(case.parent_id)
|
|
96
|
+
# The recorder state should always be consistent
|
|
97
|
+
assert parent is not None, "Parent does not exist"
|
|
98
|
+
return parent.value
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def find_related(self, *, case_id: str) -> Iterator[Case]:
|
|
102
|
+
"""Iterate over all cases in the tree, starting from the root."""
|
|
103
|
+
seen = {case_id}
|
|
104
|
+
|
|
105
|
+
# First, find the root by going up
|
|
106
|
+
current_id = case_id
|
|
107
|
+
while True:
|
|
108
|
+
current_node = self.cases.get(current_id)
|
|
109
|
+
if current_node is None or current_node.parent_id is None:
|
|
110
|
+
root_id = current_id
|
|
111
|
+
break
|
|
112
|
+
current_id = current_node.parent_id
|
|
113
|
+
|
|
114
|
+
# Then traverse the whole tree from root
|
|
115
|
+
def traverse(node_id: str) -> Iterator[Case]:
|
|
116
|
+
# Get all children
|
|
117
|
+
for case_id, node in self.cases.items():
|
|
118
|
+
if node.parent_id == node_id and case_id not in seen:
|
|
119
|
+
seen.add(case_id)
|
|
120
|
+
yield node.value
|
|
121
|
+
# Recurse into children
|
|
122
|
+
yield from traverse(case_id)
|
|
123
|
+
|
|
124
|
+
# Start traversal from root
|
|
125
|
+
root_node = self.cases.get(root_id)
|
|
126
|
+
if root_node and root_id not in seen:
|
|
127
|
+
seen.add(root_id)
|
|
128
|
+
yield root_node.value
|
|
129
|
+
yield from traverse(root_id)
|
|
130
|
+
|
|
131
|
+
def find_response(self, *, case_id: str) -> Response | None:
|
|
132
|
+
"""Retrieve the API response for a given test case, if available."""
|
|
133
|
+
interaction = self.interactions.get(case_id)
|
|
134
|
+
if interaction is None or interaction.response is None:
|
|
135
|
+
return None
|
|
136
|
+
return interaction.response
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class CaseNode:
|
|
141
|
+
"""Represents a test case and its parent-child relationship."""
|
|
142
|
+
|
|
143
|
+
value: Case
|
|
144
|
+
parent_id: str | None
|
|
145
|
+
# Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
|
|
146
|
+
# and outside of the implemented transition logic (e.g. Open API links)
|
|
147
|
+
transition: Transition | None
|
|
148
|
+
is_transition_applied: bool
|
|
149
|
+
|
|
150
|
+
__slots__ = ("value", "parent_id", "transition", "is_transition_applied")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class CheckNode:
|
|
155
|
+
name: str
|
|
156
|
+
status: Status
|
|
157
|
+
failure_info: CheckFailureInfo | None
|
|
158
|
+
|
|
159
|
+
__slots__ = ("name", "status", "failure_info")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class CheckFailureInfo:
|
|
164
|
+
code_sample: str
|
|
165
|
+
failure: Failure
|
|
166
|
+
|
|
167
|
+
__slots__ = ("code_sample", "failure")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def serialize_payload(payload: bytes) -> str:
|
|
171
|
+
return base64.b64encode(payload).decode()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(repr=False)
|
|
175
|
+
class Request:
|
|
176
|
+
"""Request data extracted from `Case`."""
|
|
177
|
+
|
|
178
|
+
method: str
|
|
179
|
+
uri: str
|
|
180
|
+
body: bytes | None
|
|
181
|
+
body_size: int | None
|
|
182
|
+
headers: dict[str, list[str]]
|
|
183
|
+
|
|
184
|
+
__slots__ = ("method", "uri", "body", "body_size", "headers", "_encoded_body_cache")
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
method: str,
|
|
189
|
+
uri: str,
|
|
190
|
+
body: bytes | None,
|
|
191
|
+
body_size: int | None,
|
|
192
|
+
headers: dict[str, list[str]],
|
|
193
|
+
):
|
|
194
|
+
self.method = method
|
|
195
|
+
self.uri = uri
|
|
196
|
+
self.body = body
|
|
197
|
+
self.body_size = body_size
|
|
198
|
+
self.headers = headers
|
|
199
|
+
self._encoded_body_cache: str | None = None
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
|
|
203
|
+
"""A prepared request version is already stored in `requests.Response`."""
|
|
204
|
+
body = prepared.body
|
|
205
|
+
|
|
206
|
+
if isinstance(body, str):
|
|
207
|
+
# can be a string for `application/x-www-form-urlencoded`
|
|
208
|
+
body = body.encode("utf-8")
|
|
209
|
+
|
|
210
|
+
# these values have `str` type at this point
|
|
211
|
+
uri = cast(str, prepared.url)
|
|
212
|
+
method = cast(str, prepared.method)
|
|
213
|
+
return cls(
|
|
214
|
+
uri=uri,
|
|
215
|
+
method=method,
|
|
216
|
+
headers={key: [value] for (key, value) in prepared.headers.items()},
|
|
217
|
+
body=body,
|
|
218
|
+
body_size=len(body) if body is not None else None,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def encoded_body(self) -> str | None:
|
|
223
|
+
if self.body is not None:
|
|
224
|
+
if self._encoded_body_cache is None:
|
|
225
|
+
self._encoded_body_cache = serialize_payload(self.body)
|
|
226
|
+
return self._encoded_body_cache
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class Interaction:
|
|
232
|
+
"""Represents a single interaction with the tested application."""
|
|
233
|
+
|
|
234
|
+
request: Request
|
|
235
|
+
response: Response | None
|
|
236
|
+
timestamp: float
|
|
237
|
+
|
|
238
|
+
__slots__ = ("request", "response", "timestamp")
|
|
239
|
+
|
|
240
|
+
def __init__(self, request: Request, response: Response | None) -> None:
|
|
241
|
+
self.request = request
|
|
242
|
+
self.response = response
|
|
243
|
+
self.timestamp = time.time()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class FailureData:
|
|
248
|
+
"""Details about a test failure, including the case and its context."""
|
|
249
|
+
|
|
250
|
+
case: Case
|
|
251
|
+
headers: dict[str, str]
|
|
252
|
+
verify: bool
|
|
253
|
+
|
|
254
|
+
__slots__ = ("case", "headers", "verify")
|
schemathesis/errors.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Public Schemathesis errors."""
|
|
2
|
+
|
|
3
|
+
from schemathesis.core.errors import (
|
|
4
|
+
HookError,
|
|
5
|
+
IncorrectUsage,
|
|
6
|
+
InfiniteRecursiveReference,
|
|
7
|
+
InternalError,
|
|
8
|
+
InvalidHeadersExample,
|
|
9
|
+
InvalidRateLimit,
|
|
10
|
+
InvalidRegexPattern,
|
|
11
|
+
InvalidRegexType,
|
|
12
|
+
InvalidSchema,
|
|
13
|
+
InvalidStateMachine,
|
|
14
|
+
InvalidTransition,
|
|
15
|
+
LoaderError,
|
|
16
|
+
NoLinksFound,
|
|
17
|
+
OperationNotFound,
|
|
18
|
+
SchemathesisError,
|
|
19
|
+
SerializationError,
|
|
20
|
+
SerializationNotPossible,
|
|
21
|
+
TransitionValidationError,
|
|
22
|
+
UnboundPrefix,
|
|
23
|
+
UnresolvableReference,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"HookError",
|
|
28
|
+
"IncorrectUsage",
|
|
29
|
+
"InfiniteRecursiveReference",
|
|
30
|
+
"InternalError",
|
|
31
|
+
"InvalidHeadersExample",
|
|
32
|
+
"InvalidRateLimit",
|
|
33
|
+
"InvalidRegexPattern",
|
|
34
|
+
"InvalidRegexType",
|
|
35
|
+
"InvalidSchema",
|
|
36
|
+
"InvalidStateMachine",
|
|
37
|
+
"InvalidTransition",
|
|
38
|
+
"LoaderError",
|
|
39
|
+
"OperationNotFound",
|
|
40
|
+
"NoLinksFound",
|
|
41
|
+
"SchemathesisError",
|
|
42
|
+
"SerializationError",
|
|
43
|
+
"SerializationNotPossible",
|
|
44
|
+
"TransitionValidationError",
|
|
45
|
+
"UnboundPrefix",
|
|
46
|
+
"UnresolvableReference",
|
|
47
|
+
]
|
schemathesis/filters.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from functools import partial
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
|
|
11
|
+
|
|
12
|
+
from schemathesis.core.errors import IncorrectUsage
|
|
13
|
+
from schemathesis.core.transforms import resolve_pointer
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from schemathesis.schemas import APIOperation
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HasAPIOperation(Protocol):
|
|
20
|
+
operation: APIOperation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MatcherFunc = Callable[[HasAPIOperation], bool]
|
|
24
|
+
FilterValue = Union[str, List[str]]
|
|
25
|
+
RegexValue = Union[str, re.Pattern]
|
|
26
|
+
ERROR_EXPECTED_AND_REGEX = "Passing expected value and regex simultaneously is not allowed"
|
|
27
|
+
ERROR_EMPTY_FILTER = "Filter can not be empty"
|
|
28
|
+
ERROR_FILTER_EXISTS = "Filter already exists"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(repr=False, frozen=True)
|
|
32
|
+
class Matcher:
|
|
33
|
+
"""Encapsulates matching logic by various criteria."""
|
|
34
|
+
|
|
35
|
+
func: Callable[..., bool] = field(hash=False, compare=False)
|
|
36
|
+
# A short description of a matcher. Primarily exists for debugging purposes
|
|
37
|
+
label: str = field(hash=False, compare=False)
|
|
38
|
+
# Compare & hash matchers by a pre-computed hash value
|
|
39
|
+
_hash: int
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return f"<{self.__class__.__name__}: {self.label}>"
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def for_function(cls, func: MatcherFunc) -> Matcher:
|
|
46
|
+
"""Matcher that uses the given function for matching operations."""
|
|
47
|
+
return cls(func, label=func.__name__, _hash=hash(func))
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def for_value(cls, attribute: str, expected: FilterValue) -> Matcher:
|
|
51
|
+
"""Matcher that checks whether the specified attribute has the expected value."""
|
|
52
|
+
if isinstance(expected, list):
|
|
53
|
+
func = partial(by_value_list, attribute=attribute, expected=expected)
|
|
54
|
+
else:
|
|
55
|
+
func = partial(by_value, attribute=attribute, expected=expected)
|
|
56
|
+
label = f"{attribute}={expected!r}"
|
|
57
|
+
return cls(func, label=label, _hash=hash(label))
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
|
|
61
|
+
"""Matcher that checks whether the specified attribute has the provided regex."""
|
|
62
|
+
if isinstance(regex, str):
|
|
63
|
+
flags: re.RegexFlag | int
|
|
64
|
+
if attribute == "method":
|
|
65
|
+
flags = re.IGNORECASE
|
|
66
|
+
else:
|
|
67
|
+
flags = 0
|
|
68
|
+
regex = re.compile(regex, flags=flags)
|
|
69
|
+
func = partial(by_regex, attribute=attribute, regex=regex)
|
|
70
|
+
label = f"{attribute}_regex={regex!r}"
|
|
71
|
+
return cls(func, label=label, _hash=hash(label))
|
|
72
|
+
|
|
73
|
+
def match(self, ctx: HasAPIOperation) -> bool:
|
|
74
|
+
"""Whether matcher matches the given operation."""
|
|
75
|
+
return self.func(ctx)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_operation_attribute(operation: APIOperation, attribute: str) -> str | list[str] | None:
|
|
79
|
+
if attribute == "tag":
|
|
80
|
+
return operation.tags
|
|
81
|
+
if attribute == "operation_id":
|
|
82
|
+
return operation.definition.raw.get("operationId")
|
|
83
|
+
# Just uppercase `method`
|
|
84
|
+
value = getattr(operation, attribute)
|
|
85
|
+
if attribute == "method":
|
|
86
|
+
value = value.upper()
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def by_value(ctx: HasAPIOperation, attribute: str, expected: str) -> bool:
|
|
91
|
+
value = get_operation_attribute(ctx.operation, attribute)
|
|
92
|
+
if value is None:
|
|
93
|
+
return False
|
|
94
|
+
if isinstance(value, list):
|
|
95
|
+
return any(entry == expected for entry in value)
|
|
96
|
+
return value == expected
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def by_value_list(ctx: HasAPIOperation, attribute: str, expected: list[str]) -> bool:
|
|
100
|
+
value = get_operation_attribute(ctx.operation, attribute)
|
|
101
|
+
if value is None:
|
|
102
|
+
return False
|
|
103
|
+
if isinstance(value, list):
|
|
104
|
+
return any(entry in expected for entry in value)
|
|
105
|
+
return value in expected
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
|
|
109
|
+
value = get_operation_attribute(ctx.operation, attribute)
|
|
110
|
+
if value is None:
|
|
111
|
+
return False
|
|
112
|
+
if isinstance(value, list):
|
|
113
|
+
return any(bool(regex.search(entry)) for entry in value)
|
|
114
|
+
return bool(regex.search(value))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(repr=False, frozen=True)
|
|
118
|
+
class Filter:
|
|
119
|
+
"""Match API operations against a list of matchers."""
|
|
120
|
+
|
|
121
|
+
matchers: tuple[Matcher, ...]
|
|
122
|
+
|
|
123
|
+
__slots__ = ("matchers",)
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
inner = " && ".join(matcher.label for matcher in self.matchers)
|
|
127
|
+
return f"<{self.__class__.__name__}: [{inner}]>"
|
|
128
|
+
|
|
129
|
+
def match(self, ctx: HasAPIOperation) -> bool:
|
|
130
|
+
"""Whether the operation matches the filter.
|
|
131
|
+
|
|
132
|
+
Returns `True` only if all matchers matched.
|
|
133
|
+
"""
|
|
134
|
+
return all(matcher.match(ctx) for matcher in self.matchers)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class FilterSet:
|
|
139
|
+
"""Combines multiple filters to apply inclusion and exclusion rules on API operations."""
|
|
140
|
+
|
|
141
|
+
_includes: set[Filter]
|
|
142
|
+
_excludes: set[Filter]
|
|
143
|
+
|
|
144
|
+
__slots__ = ("_includes", "_excludes")
|
|
145
|
+
|
|
146
|
+
def __init__(self, _includes: set[Filter] | None = None, _excludes: set[Filter] | None = None) -> None:
|
|
147
|
+
self._includes = _includes or set()
|
|
148
|
+
self._excludes = _excludes or set()
|
|
149
|
+
|
|
150
|
+
def clone(self) -> FilterSet:
|
|
151
|
+
return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
|
|
152
|
+
|
|
153
|
+
def applies_to(self, operation: APIOperation) -> bool:
|
|
154
|
+
return self.match(SimpleNamespace(operation=operation))
|
|
155
|
+
|
|
156
|
+
def match(self, ctx: HasAPIOperation) -> bool:
|
|
157
|
+
"""Determines whether the given operation should be included based on the defined filters.
|
|
158
|
+
|
|
159
|
+
Returns True if the operation:
|
|
160
|
+
- matches at least one INCLUDE filter OR no INCLUDE filters defined;
|
|
161
|
+
- does not match any EXCLUDE filter;
|
|
162
|
+
False otherwise.
|
|
163
|
+
"""
|
|
164
|
+
# Exclude early if the operation is excluded by at least one EXCLUDE filter
|
|
165
|
+
for filter_ in self._excludes:
|
|
166
|
+
if filter_.match(ctx):
|
|
167
|
+
return False
|
|
168
|
+
if not self._includes:
|
|
169
|
+
# No includes - nothing to filter out, include the operation
|
|
170
|
+
return True
|
|
171
|
+
# Otherwise check if the operation is included by at least one INCLUDE filter
|
|
172
|
+
return any(filter_.match(ctx) for filter_ in self._includes)
|
|
173
|
+
|
|
174
|
+
def is_empty(self) -> bool:
|
|
175
|
+
"""Whether the filter set does not contain any filters."""
|
|
176
|
+
return not self._includes and not self._excludes
|
|
177
|
+
|
|
178
|
+
def clear(self) -> None:
|
|
179
|
+
self._includes.clear()
|
|
180
|
+
self._excludes.clear()
|
|
181
|
+
|
|
182
|
+
def include(
|
|
183
|
+
self,
|
|
184
|
+
func: MatcherFunc | None = None,
|
|
185
|
+
*,
|
|
186
|
+
name: FilterValue | None = None,
|
|
187
|
+
name_regex: RegexValue | None = None,
|
|
188
|
+
method: FilterValue | None = None,
|
|
189
|
+
method_regex: RegexValue | None = None,
|
|
190
|
+
path: FilterValue | None = None,
|
|
191
|
+
path_regex: RegexValue | None = None,
|
|
192
|
+
tag: FilterValue | None = None,
|
|
193
|
+
tag_regex: RegexValue | None = None,
|
|
194
|
+
operation_id: FilterValue | None = None,
|
|
195
|
+
operation_id_regex: RegexValue | None = None,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Add a new INCLUDE filter."""
|
|
198
|
+
self._add_filter(
|
|
199
|
+
True,
|
|
200
|
+
func=func,
|
|
201
|
+
name=name,
|
|
202
|
+
name_regex=name_regex,
|
|
203
|
+
method=method,
|
|
204
|
+
method_regex=method_regex,
|
|
205
|
+
path=path,
|
|
206
|
+
path_regex=path_regex,
|
|
207
|
+
tag=tag,
|
|
208
|
+
tag_regex=tag_regex,
|
|
209
|
+
operation_id=operation_id,
|
|
210
|
+
operation_id_regex=operation_id_regex,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def exclude(
|
|
214
|
+
self,
|
|
215
|
+
func: MatcherFunc | None = None,
|
|
216
|
+
*,
|
|
217
|
+
name: FilterValue | None = None,
|
|
218
|
+
name_regex: RegexValue | None = None,
|
|
219
|
+
method: FilterValue | None = None,
|
|
220
|
+
method_regex: RegexValue | None = None,
|
|
221
|
+
path: FilterValue | None = None,
|
|
222
|
+
path_regex: RegexValue | None = None,
|
|
223
|
+
tag: FilterValue | None = None,
|
|
224
|
+
tag_regex: RegexValue | None = None,
|
|
225
|
+
operation_id: FilterValue | None = None,
|
|
226
|
+
operation_id_regex: RegexValue | None = None,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Add a new EXCLUDE filter."""
|
|
229
|
+
self._add_filter(
|
|
230
|
+
False,
|
|
231
|
+
func=func,
|
|
232
|
+
name=name,
|
|
233
|
+
name_regex=name_regex,
|
|
234
|
+
method=method,
|
|
235
|
+
method_regex=method_regex,
|
|
236
|
+
path=path,
|
|
237
|
+
path_regex=path_regex,
|
|
238
|
+
tag=tag,
|
|
239
|
+
tag_regex=tag_regex,
|
|
240
|
+
operation_id=operation_id,
|
|
241
|
+
operation_id_regex=operation_id_regex,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _add_filter(
|
|
245
|
+
self,
|
|
246
|
+
include: bool,
|
|
247
|
+
*,
|
|
248
|
+
func: MatcherFunc | None = None,
|
|
249
|
+
name: FilterValue | None = None,
|
|
250
|
+
name_regex: RegexValue | None = None,
|
|
251
|
+
method: FilterValue | None = None,
|
|
252
|
+
method_regex: RegexValue | None = None,
|
|
253
|
+
path: FilterValue | None = None,
|
|
254
|
+
path_regex: RegexValue | None = None,
|
|
255
|
+
tag: FilterValue | None = None,
|
|
256
|
+
tag_regex: RegexValue | None = None,
|
|
257
|
+
operation_id: FilterValue | None = None,
|
|
258
|
+
operation_id_regex: RegexValue | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
matchers = []
|
|
261
|
+
if func is not None:
|
|
262
|
+
matchers.append(Matcher.for_function(func))
|
|
263
|
+
for attribute, expected, regex in (
|
|
264
|
+
("label", name, name_regex),
|
|
265
|
+
("method", method, method_regex),
|
|
266
|
+
("path", path, path_regex),
|
|
267
|
+
("tag", tag, tag_regex),
|
|
268
|
+
("operation_id", operation_id, operation_id_regex),
|
|
269
|
+
):
|
|
270
|
+
if expected is not None and regex is not None:
|
|
271
|
+
# To match anything the regex should match the expected value, hence passing them together is useless
|
|
272
|
+
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
|
273
|
+
if expected is not None:
|
|
274
|
+
if attribute == "method":
|
|
275
|
+
expected = _normalize_method(expected)
|
|
276
|
+
matchers.append(Matcher.for_value(attribute, expected))
|
|
277
|
+
if regex is not None:
|
|
278
|
+
matchers.append(Matcher.for_regex(attribute, regex))
|
|
279
|
+
|
|
280
|
+
if not matchers:
|
|
281
|
+
raise IncorrectUsage(ERROR_EMPTY_FILTER)
|
|
282
|
+
filter_ = Filter(matchers=tuple(matchers))
|
|
283
|
+
if filter_ in self._includes or filter_ in self._excludes:
|
|
284
|
+
raise IncorrectUsage(ERROR_FILTER_EXISTS)
|
|
285
|
+
if include:
|
|
286
|
+
self._includes.add(filter_)
|
|
287
|
+
else:
|
|
288
|
+
self._excludes.add(filter_)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _normalize_method(value: FilterValue) -> FilterValue:
|
|
292
|
+
if isinstance(value, list):
|
|
293
|
+
return [item.upper() for item in value]
|
|
294
|
+
return value.upper()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def attach_filter_chain(
|
|
298
|
+
target: Callable,
|
|
299
|
+
attribute: str,
|
|
300
|
+
filter_func: Callable[..., None],
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Attach a filtering function to an object, which allows chaining of filter criteria.
|
|
303
|
+
|
|
304
|
+
For example:
|
|
305
|
+
|
|
306
|
+
>>> def auth(): ...
|
|
307
|
+
>>> filter_set = FilterSet()
|
|
308
|
+
>>> attach_filter_chain(auth, "apply_to", filter_set.include)
|
|
309
|
+
>>> auth.apply_to(method="GET", path="/users/")
|
|
310
|
+
|
|
311
|
+
This will add a new `apply_to` method to `auth` that matches only the `GET /users/` operation.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def proxy(
|
|
315
|
+
func: MatcherFunc | None = None,
|
|
316
|
+
*,
|
|
317
|
+
name: FilterValue | None = None,
|
|
318
|
+
name_regex: str | None = None,
|
|
319
|
+
method: FilterValue | None = None,
|
|
320
|
+
method_regex: str | None = None,
|
|
321
|
+
tag: FilterValue | None = None,
|
|
322
|
+
tag_regex: RegexValue | None = None,
|
|
323
|
+
path: FilterValue | None = None,
|
|
324
|
+
path_regex: str | None = None,
|
|
325
|
+
operation_id: FilterValue | None = None,
|
|
326
|
+
operation_id_regex: RegexValue | None = None,
|
|
327
|
+
) -> Callable:
|
|
328
|
+
__tracebackhide__ = True
|
|
329
|
+
filter_func(
|
|
330
|
+
func=func,
|
|
331
|
+
name=name,
|
|
332
|
+
name_regex=name_regex,
|
|
333
|
+
method=method,
|
|
334
|
+
method_regex=method_regex,
|
|
335
|
+
tag=tag,
|
|
336
|
+
tag_regex=tag_regex,
|
|
337
|
+
path=path,
|
|
338
|
+
path_regex=path_regex,
|
|
339
|
+
operation_id=operation_id,
|
|
340
|
+
operation_id_regex=operation_id_regex,
|
|
341
|
+
)
|
|
342
|
+
return target
|
|
343
|
+
|
|
344
|
+
proxy.__qualname__ = attribute
|
|
345
|
+
proxy.__name__ = attribute
|
|
346
|
+
|
|
347
|
+
setattr(target, attribute, proxy)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def is_deprecated(ctx: HasAPIOperation) -> bool:
|
|
351
|
+
return ctx.operation.definition.raw.get("deprecated") is True
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def parse_expression(expression: str) -> tuple[str, str, Any]:
|
|
355
|
+
expression = expression.strip()
|
|
356
|
+
|
|
357
|
+
# Find the operator
|
|
358
|
+
for op in ("==", "!="):
|
|
359
|
+
try:
|
|
360
|
+
pointer, value = expression.split(op, 1)
|
|
361
|
+
break
|
|
362
|
+
except ValueError:
|
|
363
|
+
continue
|
|
364
|
+
else:
|
|
365
|
+
raise ValueError(f"Invalid expression: {expression}")
|
|
366
|
+
|
|
367
|
+
pointer = pointer.strip()
|
|
368
|
+
value = value.strip()
|
|
369
|
+
if not pointer or not value:
|
|
370
|
+
raise ValueError(f"Invalid expression: {expression}")
|
|
371
|
+
# Parse the JSON value
|
|
372
|
+
try:
|
|
373
|
+
return pointer, op, json.loads(value)
|
|
374
|
+
except json.JSONDecodeError:
|
|
375
|
+
# If it's not valid JSON, treat it as a string
|
|
376
|
+
return pointer, op, value
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
|
|
380
|
+
pointer, op, value = parse_expression(expression)
|
|
381
|
+
|
|
382
|
+
if op == "==":
|
|
383
|
+
|
|
384
|
+
def filter_function(ctx: HasAPIOperation) -> bool:
|
|
385
|
+
definition = ctx.operation.definition.raw
|
|
386
|
+
resolved = resolve_pointer(definition, pointer)
|
|
387
|
+
return resolved == value
|
|
388
|
+
else:
|
|
389
|
+
|
|
390
|
+
def filter_function(ctx: HasAPIOperation) -> bool:
|
|
391
|
+
definition = ctx.operation.definition.raw
|
|
392
|
+
resolved = resolve_pointer(definition, pointer)
|
|
393
|
+
return resolved != value
|
|
394
|
+
|
|
395
|
+
return filter_function
|