schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,7 @@ from difflib import get_close_matches
|
|
8
8
|
from hashlib import sha1
|
9
9
|
from json import JSONDecodeError
|
10
10
|
from threading import RLock
|
11
|
+
from types import SimpleNamespace
|
11
12
|
from typing import (
|
12
13
|
TYPE_CHECKING,
|
13
14
|
Any,
|
@@ -15,57 +16,41 @@ from typing import (
|
|
15
16
|
ClassVar,
|
16
17
|
Generator,
|
17
18
|
Iterable,
|
19
|
+
Iterator,
|
20
|
+
Mapping,
|
18
21
|
NoReturn,
|
19
|
-
|
20
|
-
TypeVar,
|
22
|
+
cast,
|
21
23
|
)
|
22
24
|
from urllib.parse import urlsplit
|
23
25
|
|
24
26
|
import jsonschema
|
25
|
-
from hypothesis.strategies import SearchStrategy
|
26
27
|
from packaging import version
|
28
|
+
from requests.exceptions import InvalidHeader
|
27
29
|
from requests.structures import CaseInsensitiveDict
|
28
|
-
|
29
|
-
|
30
|
-
from
|
31
|
-
from
|
32
|
-
from
|
33
|
-
from
|
34
|
-
from
|
35
|
-
from
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
|
47
|
-
from ...internal.copy import fast_deepcopy
|
48
|
-
from ...internal.jsonschema import traverse_schema
|
49
|
-
from ...internal.result import Err, Ok, Result
|
50
|
-
from ...models import APIOperation, Case, OperationDefinition
|
51
|
-
from ...schemas import BaseSchema, APIOperationMap
|
52
|
-
from ...stateful import Stateful, StatefulTest
|
53
|
-
from ...stateful.state_machine import APIStateMachine
|
54
|
-
from ...transports.content_types import is_json_media_type, parse_content_type
|
55
|
-
from ...transports.responses import get_json
|
56
|
-
from ...types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query, GenericTest
|
30
|
+
from requests.utils import check_header_validity
|
31
|
+
|
32
|
+
from schemathesis.core import NOT_SET, NotSet, Specification, media_types
|
33
|
+
from schemathesis.core.compat import RefResolutionError
|
34
|
+
from schemathesis.core.errors import InternalError, InvalidSchema, LoaderError, LoaderErrorKind, OperationNotFound
|
35
|
+
from schemathesis.core.failures import Failure, FailureGroup, MalformedJson
|
36
|
+
from schemathesis.core.result import Err, Ok, Result
|
37
|
+
from schemathesis.core.transforms import UNRESOLVABLE, deepclone, resolve_pointer, transform
|
38
|
+
from schemathesis.core.transport import Response
|
39
|
+
from schemathesis.core.validation import INVALID_HEADER_RE
|
40
|
+
from schemathesis.generation.case import Case
|
41
|
+
from schemathesis.generation.meta import CaseMetadata
|
42
|
+
from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
|
43
|
+
from schemathesis.openapi.checks import JsonSchemaError, MissingContentType
|
44
|
+
|
45
|
+
from ...generation import GenerationConfig, GenerationMode
|
46
|
+
from ...hooks import HookContext, HookDispatcher
|
47
|
+
from ...schemas import APIOperation, APIOperationMap, ApiOperationsCount, BaseSchema, OperationDefinition
|
57
48
|
from . import links, serialization
|
58
|
-
from .
|
49
|
+
from ._cache import OperationCache
|
50
|
+
from ._hypothesis import openapi_cases
|
59
51
|
from .converter import to_json_schema, to_json_schema_recursive
|
60
52
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
61
53
|
from .examples import get_strategies_from_examples
|
62
|
-
from .filters import (
|
63
|
-
should_skip_by_operation_id,
|
64
|
-
should_skip_by_tag,
|
65
|
-
should_skip_deprecated,
|
66
|
-
should_skip_endpoint,
|
67
|
-
should_skip_method,
|
68
|
-
)
|
69
54
|
from .parameters import (
|
70
55
|
OpenAPI20Body,
|
71
56
|
OpenAPI20CompositeBody,
|
@@ -74,15 +59,34 @@ from .parameters import (
|
|
74
59
|
OpenAPI30Parameter,
|
75
60
|
OpenAPIParameter,
|
76
61
|
)
|
77
|
-
from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver
|
62
|
+
from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver
|
78
63
|
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
79
64
|
from .stateful import create_state_machine
|
80
65
|
|
81
66
|
if TYPE_CHECKING:
|
82
|
-
from
|
67
|
+
from hypothesis.strategies import SearchStrategy
|
83
68
|
|
69
|
+
from ...auths import AuthStorage
|
70
|
+
from ...stateful.state_machine import APIStateMachine
|
71
|
+
|
72
|
+
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
84
73
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
85
|
-
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError,
|
74
|
+
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, RefResolutionError, InvalidSchema)
|
75
|
+
|
76
|
+
|
77
|
+
def check_header(parameter: dict[str, Any]) -> None:
|
78
|
+
name = parameter["name"]
|
79
|
+
if not name:
|
80
|
+
raise InvalidSchema("Header name should not be empty")
|
81
|
+
if not name.isascii():
|
82
|
+
# `urllib3` encodes header names to ASCII
|
83
|
+
raise InvalidSchema(f"Header name should be ASCII: {name}")
|
84
|
+
try:
|
85
|
+
check_header_validity((name, ""))
|
86
|
+
except InvalidHeader as exc:
|
87
|
+
raise InvalidSchema(str(exc)) from None
|
88
|
+
if bool(INVALID_HEADER_RE.search(name)):
|
89
|
+
raise InvalidSchema(f"Invalid header name: {name}")
|
86
90
|
|
87
91
|
|
88
92
|
@dataclass(eq=False, repr=False)
|
@@ -91,36 +95,39 @@ class BaseOpenAPISchema(BaseSchema):
|
|
91
95
|
links_field: ClassVar[str] = ""
|
92
96
|
header_required_field: ClassVar[str] = ""
|
93
97
|
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
94
|
-
|
98
|
+
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
95
99
|
_inline_reference_cache: dict[str, Any] = field(default_factory=dict)
|
96
100
|
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
97
101
|
# excessive resolving
|
98
102
|
_inline_reference_cache_lock: RLock = field(default_factory=RLock)
|
99
|
-
_override: CaseOverride | None = field(default=None)
|
100
103
|
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
101
104
|
|
102
105
|
@property
|
103
|
-
def
|
106
|
+
def specification(self) -> Specification:
|
104
107
|
raise NotImplementedError
|
105
108
|
|
106
|
-
def get_stateful_tests(
|
107
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
108
|
-
) -> Sequence[StatefulTest]:
|
109
|
-
if stateful == Stateful.links:
|
110
|
-
return links.get_links(response, operation, field=self.links_field)
|
111
|
-
return []
|
112
|
-
|
113
109
|
def __repr__(self) -> str:
|
114
110
|
info = self.raw_schema["info"]
|
115
111
|
return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
|
116
112
|
|
117
|
-
def
|
118
|
-
self,
|
119
|
-
|
120
|
-
|
113
|
+
def __iter__(self) -> Iterator[str]:
|
114
|
+
return iter(self.raw_schema.get("paths", {}))
|
115
|
+
|
116
|
+
def _get_operation_map(self, path: str) -> APIOperationMap:
|
117
|
+
cache = self._operation_cache
|
118
|
+
map = cache.get_map(path)
|
119
|
+
if map is not None:
|
120
|
+
return map
|
121
|
+
path_item = self.raw_schema.get("paths", {})[path]
|
122
|
+
scope, path_item = self._resolve_path_item(path_item)
|
123
|
+
self.dispatch_hook("before_process_path", HookContext(), path, path_item)
|
124
|
+
map = APIOperationMap(self, {})
|
125
|
+
map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
|
126
|
+
cache.insert_map(path, map)
|
127
|
+
return map
|
121
128
|
|
122
129
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
123
|
-
matches = get_close_matches(item, list(self
|
130
|
+
matches = get_close_matches(item, list(self))
|
124
131
|
self._on_missing_operation(item, exc, matches)
|
125
132
|
|
126
133
|
def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
|
@@ -129,14 +136,59 @@ class BaseOpenAPISchema(BaseSchema):
|
|
129
136
|
message += f". Did you mean `{matches[0]}`?"
|
130
137
|
raise OperationNotFound(message=message, item=item) from exc
|
131
138
|
|
132
|
-
def _should_skip(
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
139
|
+
def _should_skip(
|
140
|
+
self,
|
141
|
+
path: str,
|
142
|
+
method: str,
|
143
|
+
definition: dict[str, Any],
|
144
|
+
_ctx_cache: SimpleNamespace = SimpleNamespace(
|
145
|
+
operation=APIOperation(
|
146
|
+
method="",
|
147
|
+
path="",
|
148
|
+
label="",
|
149
|
+
definition=OperationDefinition(raw=None, resolved=None, scope=""),
|
150
|
+
schema=None, # type: ignore
|
151
|
+
)
|
152
|
+
),
|
153
|
+
) -> bool:
|
154
|
+
if method not in HTTP_METHODS:
|
155
|
+
return True
|
156
|
+
if self.filter_set.is_empty():
|
157
|
+
return False
|
158
|
+
path = self.get_full_path(path)
|
159
|
+
# Attribute assignment is way faster than creating a new namespace every time
|
160
|
+
operation = _ctx_cache.operation
|
161
|
+
operation.method = method
|
162
|
+
operation.path = path
|
163
|
+
operation.label = f"{method.upper()} {path}"
|
164
|
+
operation.definition.raw = definition
|
165
|
+
operation.definition.resolved = definition
|
166
|
+
operation.schema = self
|
167
|
+
return not self.filter_set.match(_ctx_cache)
|
168
|
+
|
169
|
+
def _do_count_operations(self) -> ApiOperationsCount:
|
170
|
+
counter = ApiOperationsCount()
|
171
|
+
try:
|
172
|
+
paths = self.raw_schema["paths"]
|
173
|
+
except KeyError:
|
174
|
+
return counter
|
175
|
+
|
176
|
+
resolve = self.resolver.resolve
|
177
|
+
should_skip = self._should_skip
|
178
|
+
|
179
|
+
for path, path_item in paths.items():
|
180
|
+
try:
|
181
|
+
if "$ref" in path_item:
|
182
|
+
_, path_item = resolve(path_item["$ref"])
|
183
|
+
for method, definition in path_item.items():
|
184
|
+
if method not in HTTP_METHODS:
|
185
|
+
continue
|
186
|
+
counter.total += 1
|
187
|
+
if not should_skip(path, method, definition):
|
188
|
+
counter.selected += 1
|
189
|
+
except SCHEMA_PARSING_ERRORS:
|
190
|
+
continue
|
191
|
+
return counter
|
140
192
|
|
141
193
|
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
142
194
|
try:
|
@@ -144,40 +196,30 @@ class BaseOpenAPISchema(BaseSchema):
|
|
144
196
|
except KeyError:
|
145
197
|
return
|
146
198
|
resolve = self.resolver.resolve
|
147
|
-
|
148
|
-
|
149
|
-
if should_skip_endpoint(full_path, self.endpoint):
|
150
|
-
continue
|
199
|
+
should_skip = self._should_skip
|
200
|
+
for path, path_item in paths.items():
|
151
201
|
try:
|
152
|
-
if "$ref" in
|
153
|
-
_,
|
154
|
-
else:
|
155
|
-
resolved_methods = methods
|
202
|
+
if "$ref" in path_item:
|
203
|
+
_, path_item = resolve(path_item["$ref"])
|
156
204
|
# Straightforward iteration is faster than converting to a set & calculating length.
|
157
|
-
for method, definition in
|
158
|
-
if
|
205
|
+
for method, definition in path_item.items():
|
206
|
+
if should_skip(path, method, definition):
|
159
207
|
continue
|
160
208
|
yield definition
|
161
209
|
except SCHEMA_PARSING_ERRORS:
|
162
210
|
# Ignore errors
|
163
211
|
continue
|
164
212
|
|
165
|
-
@property
|
166
|
-
def operations_count(self) -> int:
|
167
|
-
total = 0
|
168
|
-
# Do not build a list from it
|
169
|
-
for _ in self._operation_iter():
|
170
|
-
total += 1
|
171
|
-
return total
|
172
|
-
|
173
213
|
@property
|
174
214
|
def links_count(self) -> int:
|
175
215
|
total = 0
|
216
|
+
resolve = self.resolver.resolve
|
217
|
+
links_field = self.links_field
|
176
218
|
for definition in self._operation_iter():
|
177
219
|
for response in definition.get("responses", {}).values():
|
178
220
|
if "$ref" in response:
|
179
|
-
_, response =
|
180
|
-
defined_links = response.get(
|
221
|
+
_, response = resolve(response["$ref"])
|
222
|
+
defined_links = response.get(links_field)
|
181
223
|
if defined_links is not None:
|
182
224
|
total += len(defined_links)
|
183
225
|
return total
|
@@ -189,22 +231,40 @@ class BaseOpenAPISchema(BaseSchema):
|
|
189
231
|
headers: dict[str, str] | None = None,
|
190
232
|
cookies: dict[str, str] | None = None,
|
191
233
|
path_parameters: dict[str, str] | None = None,
|
192
|
-
) -> Callable[[
|
234
|
+
) -> Callable[[Callable], Callable]:
|
193
235
|
"""Override Open API parameters with fixed values."""
|
194
236
|
|
195
|
-
def _add_override(test:
|
237
|
+
def _add_override(test: Callable) -> Callable:
|
196
238
|
check_no_override_mark(test)
|
197
|
-
override =
|
239
|
+
override = Override(
|
198
240
|
query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
|
199
241
|
)
|
200
|
-
|
242
|
+
OverrideMark.set(test, override)
|
201
243
|
return test
|
202
244
|
|
203
245
|
return _add_override
|
204
246
|
|
247
|
+
def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
|
248
|
+
while "$ref" in value:
|
249
|
+
_, value = self.resolver.resolve(value["$ref"])
|
250
|
+
return value
|
251
|
+
|
252
|
+
def _resolve_shared_parameters(self, path_item: Mapping[str, Any]) -> list[dict[str, Any]]:
|
253
|
+
return self.resolver.resolve_all(path_item.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
|
254
|
+
|
255
|
+
def _resolve_operation(self, operation: dict[str, Any]) -> dict[str, Any]:
|
256
|
+
return self.resolver.resolve_all(operation, RECURSION_DEPTH_LIMIT - 8)
|
257
|
+
|
258
|
+
def _collect_operation_parameters(
|
259
|
+
self, path_item: Mapping[str, Any], operation: dict[str, Any]
|
260
|
+
) -> list[OpenAPIParameter]:
|
261
|
+
shared_parameters = self._resolve_shared_parameters(path_item)
|
262
|
+
parameters = operation.get("parameters", ())
|
263
|
+
return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
|
264
|
+
|
205
265
|
def get_all_operations(
|
206
|
-
self,
|
207
|
-
) -> Generator[Result[APIOperation,
|
266
|
+
self, generation_config: GenerationConfig | None = None
|
267
|
+
) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
208
268
|
"""Iterate over all operations defined in the API.
|
209
269
|
|
210
270
|
Each yielded item is either `Ok` or `Err`, depending on the presence of errors during schema processing.
|
@@ -225,62 +285,59 @@ class BaseOpenAPISchema(BaseSchema):
|
|
225
285
|
paths = self.raw_schema["paths"]
|
226
286
|
except KeyError as exc:
|
227
287
|
# This field is optional in Open API 3.1
|
228
|
-
if version.parse(self.
|
288
|
+
if version.parse(self.specification.version) >= version.parse("3.1"):
|
229
289
|
return
|
230
290
|
# Missing `paths` is not recoverable
|
231
291
|
self._raise_invalid_schema(exc)
|
232
292
|
|
233
293
|
context = HookContext()
|
234
|
-
|
294
|
+
# Optimization: local variables are faster than attribute access
|
295
|
+
dispatch_hook = self.dispatch_hook
|
296
|
+
resolve_path_item = self._resolve_path_item
|
297
|
+
resolve_shared_parameters = self._resolve_shared_parameters
|
298
|
+
resolve_operation = self._resolve_operation
|
299
|
+
should_skip = self._should_skip
|
300
|
+
collect_parameters = self.collect_parameters
|
301
|
+
make_operation = self.make_operation
|
302
|
+
for path, path_item in paths.items():
|
235
303
|
method = None
|
236
304
|
try:
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
for method, definition in raw_methods.items():
|
244
|
-
try:
|
245
|
-
# Setting a low recursion limit doesn't solve the problem with recursive references & inlining
|
246
|
-
# too much but decreases the number of cases when Schemathesis stuck on this step.
|
247
|
-
self.resolver.push_scope(scope)
|
248
|
-
try:
|
249
|
-
resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
|
250
|
-
finally:
|
251
|
-
self.resolver.pop_scope()
|
252
|
-
# Only method definitions are parsed
|
253
|
-
if self._should_skip(method, resolved_definition):
|
305
|
+
dispatch_hook("before_process_path", context, path, path_item)
|
306
|
+
scope, path_item = resolve_path_item(path_item)
|
307
|
+
with in_scope(self.resolver, scope):
|
308
|
+
shared_parameters = resolve_shared_parameters(path_item)
|
309
|
+
for method, entry in path_item.items():
|
310
|
+
if method not in HTTP_METHODS:
|
254
311
|
continue
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
312
|
+
try:
|
313
|
+
resolved = resolve_operation(entry)
|
314
|
+
if should_skip(path, method, resolved):
|
315
|
+
continue
|
316
|
+
parameters = resolved.get("parameters", ())
|
317
|
+
parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
|
318
|
+
operation = make_operation(
|
319
|
+
path,
|
320
|
+
method,
|
321
|
+
parameters,
|
322
|
+
entry,
|
323
|
+
resolved,
|
324
|
+
scope,
|
325
|
+
with_security_parameters=generation_config.with_security_parameters
|
326
|
+
if generation_config
|
327
|
+
else None,
|
328
|
+
)
|
329
|
+
yield Ok(operation)
|
330
|
+
except SCHEMA_PARSING_ERRORS as exc:
|
331
|
+
yield self._into_err(exc, path, method)
|
275
332
|
except SCHEMA_PARSING_ERRORS as exc:
|
276
333
|
yield self._into_err(exc, path, method)
|
277
334
|
|
278
|
-
def _into_err(self, error: Exception, path: str | None, method: str | None) -> Err[
|
335
|
+
def _into_err(self, error: Exception, path: str | None, method: str | None) -> Err[InvalidSchema]:
|
279
336
|
__tracebackhide__ = True
|
280
337
|
try:
|
281
338
|
full_path = self.get_full_path(path) if isinstance(path, str) else None
|
282
339
|
self._raise_invalid_schema(error, full_path, path, method)
|
283
|
-
except
|
340
|
+
except InvalidSchema as exc:
|
284
341
|
return Err(exc)
|
285
342
|
|
286
343
|
def _raise_invalid_schema(
|
@@ -291,17 +348,15 @@ class BaseOpenAPISchema(BaseSchema):
|
|
291
348
|
method: str | None = None,
|
292
349
|
) -> NoReturn:
|
293
350
|
__tracebackhide__ = True
|
294
|
-
if isinstance(error,
|
295
|
-
raise
|
351
|
+
if isinstance(error, RefResolutionError):
|
352
|
+
raise InvalidSchema.from_reference_resolution_error(
|
296
353
|
error, path=path, method=method, full_path=full_path
|
297
354
|
) from None
|
298
355
|
try:
|
299
356
|
self.validate()
|
300
357
|
except jsonschema.ValidationError as exc:
|
301
|
-
raise
|
302
|
-
|
303
|
-
) from None
|
304
|
-
raise OperationSchemaError(SCHEMA_ERROR_MESSAGE, path=path, method=method, full_path=full_path) from error
|
358
|
+
raise InvalidSchema.from_jsonschema_error(exc, path=path, method=method, full_path=full_path) from None
|
359
|
+
raise InvalidSchema(SCHEMA_ERROR_MESSAGE, path=path, method=method, full_path=full_path) from error
|
305
360
|
|
306
361
|
def validate(self) -> None:
|
307
362
|
with suppress(TypeError):
|
@@ -320,35 +375,44 @@ class BaseOpenAPISchema(BaseSchema):
|
|
320
375
|
"""
|
321
376
|
raise NotImplementedError
|
322
377
|
|
323
|
-
def
|
324
|
-
#
|
325
|
-
#
|
326
|
-
#
|
378
|
+
def _resolve_path_item(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
379
|
+
# The path item could be behind a reference
|
380
|
+
# In this case, we need to resolve it to get the proper scope for reference inside the item.
|
381
|
+
# It is mostly for validating responses.
|
327
382
|
if "$ref" in methods:
|
328
|
-
return
|
329
|
-
return self.resolver.resolution_scope,
|
383
|
+
return self.resolver.resolve(methods["$ref"])
|
384
|
+
return self.resolver.resolution_scope, methods
|
330
385
|
|
331
386
|
def make_operation(
|
332
387
|
self,
|
333
388
|
path: str,
|
334
389
|
method: str,
|
335
390
|
parameters: list[OpenAPIParameter],
|
336
|
-
|
391
|
+
raw: dict[str, Any],
|
392
|
+
resolved: dict[str, Any],
|
393
|
+
scope: str,
|
394
|
+
with_security_parameters: bool | None = None,
|
337
395
|
) -> APIOperation:
|
338
396
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
339
397
|
__tracebackhide__ = True
|
340
398
|
base_url = self.get_base_url()
|
341
|
-
operation: APIOperation[OpenAPIParameter
|
399
|
+
operation: APIOperation[OpenAPIParameter] = APIOperation(
|
342
400
|
path=path,
|
343
401
|
method=method,
|
344
|
-
definition=
|
402
|
+
definition=OperationDefinition(raw, resolved, scope),
|
345
403
|
base_url=base_url,
|
346
404
|
app=self.app,
|
347
405
|
schema=self,
|
348
406
|
)
|
349
407
|
for parameter in parameters:
|
350
408
|
operation.add_parameter(parameter)
|
351
|
-
|
409
|
+
with_security_parameters = (
|
410
|
+
with_security_parameters
|
411
|
+
if with_security_parameters is not None
|
412
|
+
else self.generation_config.with_security_parameters
|
413
|
+
)
|
414
|
+
if with_security_parameters:
|
415
|
+
self.security.process_definitions(self.raw_schema, operation, self.resolver)
|
352
416
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
353
417
|
return operation
|
354
418
|
|
@@ -358,11 +422,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
358
422
|
self._resolver = InliningResolver(self.location or "", self.raw_schema)
|
359
423
|
return self._resolver
|
360
424
|
|
361
|
-
def get_content_types(self, operation: APIOperation, response:
|
425
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
362
426
|
"""Content types available for this API operation."""
|
363
427
|
raise NotImplementedError
|
364
428
|
|
365
|
-
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
429
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
366
430
|
"""Get examples from the API operation."""
|
367
431
|
raise NotImplementedError
|
368
432
|
|
@@ -376,76 +440,106 @@ class BaseOpenAPISchema(BaseSchema):
|
|
376
440
|
|
377
441
|
def get_operation_by_id(self, operation_id: str) -> APIOperation:
|
378
442
|
"""Get an `APIOperation` instance by its `operationId`."""
|
379
|
-
|
380
|
-
|
443
|
+
cache = self._operation_cache
|
444
|
+
cached = cache.get_operation_by_id(operation_id)
|
445
|
+
if cached is not None:
|
446
|
+
return cached
|
447
|
+
# Operation has not been accessed yet, need to populate the cache
|
448
|
+
if not cache.has_ids_to_definitions:
|
449
|
+
self._populate_operation_id_cache(cache)
|
381
450
|
try:
|
382
|
-
|
451
|
+
entry = cache.get_definition_by_id(operation_id)
|
383
452
|
except KeyError as exc:
|
384
|
-
matches = get_close_matches(operation_id,
|
453
|
+
matches = get_close_matches(operation_id, cache.known_operation_ids)
|
385
454
|
self._on_missing_operation(operation_id, exc, matches)
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
455
|
+
# It could've been already accessed in a different place
|
456
|
+
traversal_key = (entry.scope, entry.path, entry.method)
|
457
|
+
instance = cache.get_operation_by_traversal_key(traversal_key)
|
458
|
+
if instance is not None:
|
459
|
+
return instance
|
460
|
+
resolved = self._resolve_operation(entry.operation)
|
461
|
+
parameters = self._collect_operation_parameters(entry.path_item, resolved)
|
462
|
+
initialized = self.make_operation(entry.path, entry.method, parameters, entry.operation, resolved, entry.scope)
|
463
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=operation_id)
|
464
|
+
return initialized
|
465
|
+
|
466
|
+
def _populate_operation_id_cache(self, cache: OperationCache) -> None:
|
467
|
+
"""Collect all operation IDs from the schema."""
|
468
|
+
resolve = self.resolver.resolve
|
469
|
+
default_scope = self.resolver.resolution_scope
|
470
|
+
for path, path_item in self.raw_schema.get("paths", {}).items():
|
471
|
+
# If the path is behind a reference we have to keep the scope
|
472
|
+
# The scope is used to resolve nested components later on
|
473
|
+
if "$ref" in path_item:
|
474
|
+
scope, path_item = resolve(path_item["$ref"])
|
475
|
+
else:
|
476
|
+
scope = default_scope
|
477
|
+
for key, entry in path_item.items():
|
478
|
+
if key not in HTTP_METHODS:
|
393
479
|
continue
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
yield resolved_definition["operationId"], self.make_operation(path, method, parameters, raw_definition)
|
480
|
+
if "operationId" in entry:
|
481
|
+
cache.insert_definition_by_id(
|
482
|
+
entry["operationId"],
|
483
|
+
path=path,
|
484
|
+
method=key,
|
485
|
+
scope=scope,
|
486
|
+
path_item=path_item,
|
487
|
+
operation=entry,
|
488
|
+
)
|
404
489
|
|
405
490
|
def get_operation_by_reference(self, reference: str) -> APIOperation:
|
406
491
|
"""Get local or external `APIOperation` instance by reference.
|
407
492
|
|
408
493
|
Reference example: #/paths/~1users~1{user_id}/patch
|
409
494
|
"""
|
410
|
-
|
495
|
+
cache = self._operation_cache
|
496
|
+
cached = cache.get_operation_by_reference(reference)
|
497
|
+
if cached is not None:
|
498
|
+
return cached
|
499
|
+
scope, operation = self.resolver.resolve(reference)
|
411
500
|
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
412
501
|
path = path.replace("~1", "/").replace("~0", "~")
|
413
|
-
|
502
|
+
# Check the traversal cache as it could've been populated in other places
|
503
|
+
traversal_key = (self.resolver.resolution_scope, path, method)
|
504
|
+
cached = cache.get_operation_by_traversal_key(traversal_key)
|
505
|
+
if cached is not None:
|
506
|
+
return cached
|
507
|
+
with in_scope(self.resolver, scope):
|
508
|
+
resolved = self._resolve_operation(operation)
|
414
509
|
parent_ref, _ = reference.rsplit("/", maxsplit=1)
|
415
|
-
_,
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
raw_definition = OperationDefinition(data, resolved_definition, scope, parameters)
|
421
|
-
return self.make_operation(path, method, parameters, raw_definition)
|
510
|
+
_, path_item = self.resolver.resolve(parent_ref)
|
511
|
+
parameters = self._collect_operation_parameters(path_item, resolved)
|
512
|
+
initialized = self.make_operation(path, method, parameters, operation, resolved, scope)
|
513
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, reference=reference)
|
514
|
+
return initialized
|
422
515
|
|
423
516
|
def get_case_strategy(
|
424
517
|
self,
|
425
518
|
operation: APIOperation,
|
426
519
|
hooks: HookDispatcher | None = None,
|
427
520
|
auth_storage: AuthStorage | None = None,
|
428
|
-
|
521
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
429
522
|
generation_config: GenerationConfig | None = None,
|
430
523
|
**kwargs: Any,
|
431
524
|
) -> SearchStrategy:
|
432
|
-
return
|
525
|
+
return openapi_cases(
|
433
526
|
operation=operation,
|
434
527
|
auth_storage=auth_storage,
|
435
528
|
hooks=hooks,
|
436
|
-
|
529
|
+
generation_mode=generation_mode,
|
437
530
|
generation_config=generation_config or self.generation_config,
|
438
531
|
**kwargs,
|
439
532
|
)
|
440
533
|
|
441
534
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
442
|
-
definitions = [item for item in operation.
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
535
|
+
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
536
|
+
if self.generation_config.with_security_parameters:
|
537
|
+
security_parameters = self.security.get_security_definitions_as_parameters(
|
538
|
+
self.raw_schema, operation, self.resolver, location
|
539
|
+
)
|
540
|
+
security_parameters = [item for item in security_parameters if item["in"] == location]
|
541
|
+
if security_parameters:
|
542
|
+
definitions.extend(security_parameters)
|
449
543
|
if definitions:
|
450
544
|
return self._get_parameter_serializer(definitions)
|
451
545
|
return None
|
@@ -453,33 +547,37 @@ class BaseOpenAPISchema(BaseSchema):
|
|
453
547
|
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
454
548
|
raise NotImplementedError
|
455
549
|
|
456
|
-
def _get_response_definitions(
|
550
|
+
def _get_response_definitions(
|
551
|
+
self, operation: APIOperation, response: Response
|
552
|
+
) -> tuple[list[str], dict[str, Any]] | None:
|
457
553
|
try:
|
458
|
-
responses = operation.definition.
|
554
|
+
responses = operation.definition.raw["responses"]
|
459
555
|
except KeyError as exc:
|
460
|
-
# Possible to get if `validate_schema=False` is passed during schema creation
|
461
556
|
path = operation.path
|
462
557
|
full_path = self.get_full_path(path) if isinstance(path, str) else None
|
463
558
|
self._raise_invalid_schema(exc, full_path, path, operation.method)
|
464
559
|
status_code = str(response.status_code)
|
465
560
|
if status_code in responses:
|
466
|
-
return responses[status_code]
|
561
|
+
return self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
|
467
562
|
if "default" in responses:
|
468
|
-
return responses["default"]
|
563
|
+
return self.resolver.resolve_in_scope(responses["default"], operation.definition.scope)
|
469
564
|
return None
|
470
565
|
|
471
|
-
def get_headers(
|
472
|
-
|
473
|
-
|
566
|
+
def get_headers(
|
567
|
+
self, operation: APIOperation, response: Response
|
568
|
+
) -> tuple[list[str], dict[str, dict[str, Any]] | None] | None:
|
569
|
+
resolved = self._get_response_definitions(operation, response)
|
570
|
+
if not resolved:
|
474
571
|
return None
|
475
|
-
|
572
|
+
scopes, definitions = resolved
|
573
|
+
return scopes, definitions.get("headers")
|
476
574
|
|
477
575
|
def as_state_machine(self) -> type[APIStateMachine]:
|
478
576
|
try:
|
479
577
|
return create_state_machine(self)
|
480
578
|
except OperationNotFound as exc:
|
481
|
-
raise
|
482
|
-
|
579
|
+
raise LoaderError(
|
580
|
+
kind=LoaderErrorKind.OPEN_API_INVALID_SCHEMA,
|
483
581
|
message=f"Invalid Open API link definition: Operation `{exc.item}` not found",
|
484
582
|
) from exc
|
485
583
|
|
@@ -507,7 +605,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
507
605
|
|
508
606
|
.. code-block:: python
|
509
607
|
|
510
|
-
schema = schemathesis.
|
608
|
+
schema = schemathesis.openapi.from_url("http://0.0.0.0/schema.yaml")
|
511
609
|
|
512
610
|
schema.add_link(
|
513
611
|
source=schema["/users/"]["POST"],
|
@@ -518,46 +616,16 @@ class BaseOpenAPISchema(BaseSchema):
|
|
518
616
|
"""
|
519
617
|
if parameters is None and request_body is None:
|
520
618
|
raise ValueError("You need to provide `parameters` or `request_body`.")
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
found = True
|
532
|
-
links.add_link(
|
533
|
-
responses=definition["responses"],
|
534
|
-
links_field=self.links_field,
|
535
|
-
parameters=parameters,
|
536
|
-
request_body=request_body,
|
537
|
-
status_code=status_code,
|
538
|
-
target=target,
|
539
|
-
name=name,
|
540
|
-
)
|
541
|
-
# If methods are behind a reference, then on the next resolving they will miss the new link
|
542
|
-
# Therefore we need to modify it this way
|
543
|
-
self.raw_schema["paths"][operation][method] = definition
|
544
|
-
# The reference should be removed completely, otherwise new keys in this dictionary will be ignored
|
545
|
-
# due to the `$ref` keyword behavior
|
546
|
-
self.raw_schema["paths"][operation].pop("$ref", None)
|
547
|
-
if found:
|
548
|
-
return
|
549
|
-
name = f"{source.method.upper()} {source.path}"
|
550
|
-
# Use a name without basePath, as the user doesn't use it.
|
551
|
-
# E.g. `source=schema["/users/"]["POST"]` without a prefix
|
552
|
-
message = f"No such API operation: `{name}`."
|
553
|
-
possibilities = [
|
554
|
-
f"{op.ok().method.upper()} {op.ok().path}" for op in self.get_all_operations() if isinstance(op, Ok)
|
555
|
-
]
|
556
|
-
matches = get_close_matches(name, possibilities)
|
557
|
-
if matches:
|
558
|
-
message += f" Did you mean `{matches[0]}`?"
|
559
|
-
message += " Check if the requested API operation passes the filters in the schema."
|
560
|
-
raise ValueError(message)
|
619
|
+
links.add_link(
|
620
|
+
resolver=self.resolver,
|
621
|
+
responses=self[source.path][source.method].definition.raw["responses"],
|
622
|
+
links_field=self.links_field,
|
623
|
+
parameters=parameters,
|
624
|
+
request_body=request_body,
|
625
|
+
status_code=status_code,
|
626
|
+
target=target,
|
627
|
+
name=name,
|
628
|
+
)
|
561
629
|
|
562
630
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
563
631
|
result: dict[str, dict[str, Any]] = defaultdict(dict)
|
@@ -566,9 +634,15 @@ class BaseOpenAPISchema(BaseSchema):
|
|
566
634
|
return result
|
567
635
|
|
568
636
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
569
|
-
return operation.definition.
|
637
|
+
return operation.definition.raw.get("tags")
|
638
|
+
|
639
|
+
@property
|
640
|
+
def validator_cls(self) -> type[jsonschema.Validator]:
|
641
|
+
if self.specification.version.startswith("3.1"):
|
642
|
+
return jsonschema.Draft202012Validator
|
643
|
+
return jsonschema.Draft4Validator
|
570
644
|
|
571
|
-
def validate_response(self, operation: APIOperation, response:
|
645
|
+
def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
|
572
646
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
573
647
|
status_code = str(response.status_code)
|
574
648
|
if status_code in responses:
|
@@ -582,51 +656,52 @@ class BaseOpenAPISchema(BaseSchema):
|
|
582
656
|
if not schema:
|
583
657
|
# No schema to check against
|
584
658
|
return None
|
585
|
-
|
586
|
-
|
587
|
-
if
|
588
|
-
|
589
|
-
formatted_content_types = [f"\n- `{content_type}`" for content_type in
|
659
|
+
content_types = response.headers.get("content-type")
|
660
|
+
failures: list[Failure] = []
|
661
|
+
if content_types is None:
|
662
|
+
all_media_types = self.get_content_types(operation, response)
|
663
|
+
formatted_content_types = [f"\n- `{content_type}`" for content_type in all_media_types]
|
590
664
|
message = f"The following media types are documented in the schema:{''.join(formatted_content_types)}"
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
errors.append(exc)
|
598
|
-
if content_type and not is_json_media_type(content_type):
|
599
|
-
_maybe_raise_one_or_more(errors)
|
665
|
+
failures.append(MissingContentType(operation=operation.label, message=message, media_types=all_media_types))
|
666
|
+
content_type = None
|
667
|
+
else:
|
668
|
+
content_type = content_types[0]
|
669
|
+
if content_type and not media_types.is_json(content_type):
|
670
|
+
_maybe_raise_one_or_more(failures)
|
600
671
|
return None
|
601
672
|
try:
|
602
|
-
data =
|
673
|
+
data = response.json()
|
603
674
|
except JSONDecodeError as exc:
|
604
|
-
|
605
|
-
|
675
|
+
failures.append(MalformedJson.from_exception(operation=operation.label, exc=exc))
|
676
|
+
_maybe_raise_one_or_more(failures)
|
677
|
+
with self._validating_response(scopes) as resolver:
|
606
678
|
try:
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
679
|
+
jsonschema.validate(
|
680
|
+
data,
|
681
|
+
schema,
|
682
|
+
cls=self.validator_cls,
|
683
|
+
resolver=resolver,
|
684
|
+
# Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
|
685
|
+
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
686
|
+
)
|
687
|
+
except jsonschema.ValidationError as exc:
|
688
|
+
failures.append(
|
689
|
+
JsonSchemaError.from_exception(
|
690
|
+
operation=operation.label,
|
691
|
+
exc=exc,
|
692
|
+
output_config=operation.schema.output_config,
|
693
|
+
)
|
694
|
+
)
|
695
|
+
_maybe_raise_one_or_more(failures)
|
696
|
+
return None # explicitly return None for mypy
|
697
|
+
|
698
|
+
@contextmanager
|
699
|
+
def _validating_response(self, scopes: list[str]) -> Generator[ConvertingResolver, None, None]:
|
611
700
|
resolver = ConvertingResolver(
|
612
701
|
self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
|
613
702
|
)
|
614
|
-
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
615
|
-
cls = jsonschema.Draft202012Validator
|
616
|
-
else:
|
617
|
-
cls = jsonschema.Draft4Validator
|
618
703
|
with in_scopes(resolver, scopes):
|
619
|
-
|
620
|
-
jsonschema.validate(data, schema, cls=cls, resolver=resolver)
|
621
|
-
except jsonschema.ValidationError as exc:
|
622
|
-
exc_class = get_schema_validation_error(exc)
|
623
|
-
ctx = failures.ValidationErrorContext.from_exception(exc)
|
624
|
-
try:
|
625
|
-
raise exc_class(ctx.title, context=ctx) from exc
|
626
|
-
except Exception as exc:
|
627
|
-
errors.append(exc)
|
628
|
-
_maybe_raise_one_or_more(errors)
|
629
|
-
return None # explicitly return None for mypy
|
704
|
+
yield resolver
|
630
705
|
|
631
706
|
@property
|
632
707
|
def rewritten_components(self) -> dict[str, Any]:
|
@@ -648,7 +723,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
648
723
|
else:
|
649
724
|
break
|
650
725
|
else:
|
651
|
-
target.update(
|
726
|
+
target.update(transform(deepclone(schema), callback, self.nullable_name))
|
652
727
|
if self._inline_reference_cache:
|
653
728
|
components[INLINED_REFERENCES_KEY] = self._inline_reference_cache
|
654
729
|
self._rewritten_components = components
|
@@ -659,8 +734,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
659
734
|
|
660
735
|
Inlining components helps `hypothesis-jsonschema` generate data that involves non-resolved references.
|
661
736
|
"""
|
662
|
-
schema =
|
663
|
-
schema =
|
737
|
+
schema = deepclone(schema)
|
738
|
+
schema = transform(schema, self._rewrite_references, self.resolver)
|
664
739
|
# Only add definitions that are reachable from the schema via references
|
665
740
|
stack = [schema]
|
666
741
|
seen = set()
|
@@ -675,8 +750,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
675
750
|
pointer = reference[1:]
|
676
751
|
resolved = resolve_pointer(self.rewritten_components, pointer)
|
677
752
|
if resolved is UNRESOLVABLE:
|
678
|
-
raise
|
679
|
-
|
753
|
+
raise LoaderError(
|
754
|
+
LoaderErrorKind.OPEN_API_INVALID_SCHEMA,
|
680
755
|
message=f"Unresolvable JSON pointer in the schema: {pointer}",
|
681
756
|
)
|
682
757
|
if isinstance(resolved, dict):
|
@@ -710,7 +785,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
710
785
|
if key not in self._inline_reference_cache:
|
711
786
|
with resolver.resolving(reference) as resolved:
|
712
787
|
# Resolved object also may have references
|
713
|
-
self._inline_reference_cache[key] =
|
788
|
+
self._inline_reference_cache[key] = transform(
|
714
789
|
resolved, lambda s: self._rewrite_references(s, resolver)
|
715
790
|
)
|
716
791
|
# Rewrite the reference with the new location
|
@@ -718,13 +793,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
718
793
|
return schema
|
719
794
|
|
720
795
|
|
721
|
-
def _maybe_raise_one_or_more(
|
722
|
-
if not
|
796
|
+
def _maybe_raise_one_or_more(failures: list[Failure]) -> None:
|
797
|
+
if not failures:
|
723
798
|
return
|
724
|
-
|
725
|
-
raise
|
726
|
-
|
727
|
-
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
799
|
+
if len(failures) == 1:
|
800
|
+
raise failures[0] from None
|
801
|
+
raise FailureGroup(failures) from None
|
728
802
|
|
729
803
|
|
730
804
|
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
@@ -766,36 +840,63 @@ def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[
|
|
766
840
|
yield
|
767
841
|
|
768
842
|
|
769
|
-
|
770
|
-
|
771
|
-
) -> dict[str, APIOperationMap]:
|
772
|
-
output: dict[str, APIOperationMap] = {}
|
773
|
-
for result in operations:
|
774
|
-
if isinstance(result, Ok):
|
775
|
-
operation = result.ok()
|
776
|
-
output.setdefault(operation.path, APIOperationMap(MethodMap()))
|
777
|
-
output[operation.path][operation.method] = operation
|
778
|
-
return output
|
779
|
-
|
780
|
-
|
781
|
-
class MethodMap(CaseInsensitiveDict):
|
843
|
+
@dataclass
|
844
|
+
class MethodMap(Mapping):
|
782
845
|
"""Container for accessing API operations.
|
783
846
|
|
784
847
|
Provides a more specific error message if API operation is not found.
|
785
848
|
"""
|
786
849
|
|
850
|
+
_parent: APIOperationMap
|
851
|
+
# Reference resolution scope
|
852
|
+
_scope: str
|
853
|
+
# Methods are stored for this path
|
854
|
+
_path: str
|
855
|
+
# Storage for definitions
|
856
|
+
_path_item: CaseInsensitiveDict
|
857
|
+
|
858
|
+
__slots__ = ("_parent", "_scope", "_path", "_path_item")
|
859
|
+
|
860
|
+
def __len__(self) -> int:
|
861
|
+
return len(self._path_item)
|
862
|
+
|
863
|
+
def __iter__(self) -> Iterator[str]:
|
864
|
+
return iter(self._path_item)
|
865
|
+
|
866
|
+
def _init_operation(self, method: str) -> APIOperation:
|
867
|
+
method = method.lower()
|
868
|
+
operation = self._path_item[method]
|
869
|
+
schema = cast(BaseOpenAPISchema, self._parent._schema)
|
870
|
+
cache = schema._operation_cache
|
871
|
+
path = self._path
|
872
|
+
scope = self._scope
|
873
|
+
traversal_key = (scope, path, method)
|
874
|
+
cached = cache.get_operation_by_traversal_key(traversal_key)
|
875
|
+
if cached is not None:
|
876
|
+
return cached
|
877
|
+
schema.resolver.push_scope(scope)
|
878
|
+
try:
|
879
|
+
resolved = schema._resolve_operation(operation)
|
880
|
+
finally:
|
881
|
+
schema.resolver.pop_scope()
|
882
|
+
parameters = schema._collect_operation_parameters(self._path_item, resolved)
|
883
|
+
initialized = schema.make_operation(path, method, parameters, operation, resolved, scope)
|
884
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=resolved.get("operationId"))
|
885
|
+
return initialized
|
886
|
+
|
787
887
|
def __getitem__(self, item: str) -> APIOperation:
|
788
888
|
try:
|
789
|
-
return
|
790
|
-
except
|
889
|
+
return self._init_operation(item)
|
890
|
+
except LookupError as exc:
|
791
891
|
available_methods = ", ".join(map(str.upper, self))
|
792
|
-
message = f"Method `{item}` not found.
|
793
|
-
|
892
|
+
message = f"Method `{item.upper()}` not found."
|
893
|
+
if available_methods:
|
894
|
+
message += f" Available methods: {available_methods}"
|
895
|
+
raise LookupError(message) from exc
|
794
896
|
|
795
897
|
|
796
898
|
OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
|
797
899
|
OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE = "multipart/form-data"
|
798
|
-
C = TypeVar("C", bound=Case)
|
799
900
|
|
800
901
|
|
801
902
|
class SwaggerV20(BaseOpenAPISchema):
|
@@ -808,12 +909,9 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
808
909
|
links_field = "x-links"
|
809
910
|
|
810
911
|
@property
|
811
|
-
def
|
812
|
-
|
813
|
-
|
814
|
-
@property
|
815
|
-
def verbose_name(self) -> str:
|
816
|
-
return f"Swagger {self.spec_version}"
|
912
|
+
def specification(self) -> Specification:
|
913
|
+
version = self.raw_schema.get("swagger", "2.0")
|
914
|
+
return Specification.openapi(version=version)
|
817
915
|
|
818
916
|
def _validate(self) -> None:
|
819
917
|
SWAGGER_20_VALIDATOR.validate(self.raw_schema)
|
@@ -849,6 +947,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
849
947
|
for media_type in body_media_types:
|
850
948
|
collected.append(OpenAPI20Body(definition=parameter, media_type=media_type))
|
851
949
|
else:
|
950
|
+
if parameter["in"] in ("header", "cookie"):
|
951
|
+
check_header(parameter)
|
852
952
|
collected.append(OpenAPI20Parameter(definition=parameter))
|
853
953
|
|
854
954
|
if form_parameters:
|
@@ -859,20 +959,22 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
859
959
|
)
|
860
960
|
return collected
|
861
961
|
|
862
|
-
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
962
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
863
963
|
"""Get examples from the API operation."""
|
864
|
-
return get_strategies_from_examples(operation,
|
964
|
+
return get_strategies_from_examples(operation, **kwargs)
|
865
965
|
|
866
966
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
867
|
-
scopes, definition = self.resolver.resolve_in_scope(
|
967
|
+
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
868
968
|
schema = definition.get("schema")
|
869
969
|
if not schema:
|
870
970
|
return scopes, None
|
871
971
|
# Extra conversion to JSON Schema is needed here if there was one $ref in the input
|
872
972
|
# because it is not converted
|
873
|
-
return scopes, to_json_schema_recursive(
|
973
|
+
return scopes, to_json_schema_recursive(
|
974
|
+
schema, self.nullable_name, is_response_schema=True, update_quantifiers=False
|
975
|
+
)
|
874
976
|
|
875
|
-
def get_content_types(self, operation: APIOperation, response:
|
977
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
876
978
|
produces = operation.definition.raw.get("produces", None)
|
877
979
|
if produces:
|
878
980
|
return produces
|
@@ -882,7 +984,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
882
984
|
return serialization.serialize_swagger2_parameters(definitions)
|
883
985
|
|
884
986
|
def prepare_multipart(
|
885
|
-
self, form_data:
|
987
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
886
988
|
) -> tuple[list | None, dict[str, Any] | None]:
|
887
989
|
"""Prepare form data for sending with `requests`.
|
888
990
|
|
@@ -903,7 +1005,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
903
1005
|
else:
|
904
1006
|
files.append((name, file_value))
|
905
1007
|
|
906
|
-
for parameter in operation.
|
1008
|
+
for parameter in operation.body:
|
907
1009
|
if isinstance(parameter, OpenAPI20CompositeBody):
|
908
1010
|
for form_parameter in parameter.definition:
|
909
1011
|
name = form_parameter.name
|
@@ -918,41 +1020,35 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
918
1020
|
return files or None, data or None
|
919
1021
|
|
920
1022
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
921
|
-
return self._get_consumes_for_operation(operation.definition.
|
1023
|
+
return self._get_consumes_for_operation(operation.definition.raw)
|
922
1024
|
|
923
1025
|
def make_case(
|
924
1026
|
self,
|
925
1027
|
*,
|
926
|
-
case_cls: type[C],
|
927
1028
|
operation: APIOperation,
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
1029
|
+
method: str | None = None,
|
1030
|
+
path: str | None = None,
|
1031
|
+
path_parameters: dict[str, Any] | None = None,
|
1032
|
+
headers: dict[str, Any] | None = None,
|
1033
|
+
cookies: dict[str, Any] | None = None,
|
1034
|
+
query: dict[str, Any] | None = None,
|
1035
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
933
1036
|
media_type: str | None = None,
|
934
|
-
|
1037
|
+
meta: CaseMetadata | None = None,
|
1038
|
+
) -> Case:
|
935
1039
|
if body is not NOT_SET and media_type is None:
|
936
|
-
|
937
|
-
|
938
|
-
if len(media_types) == 1:
|
939
|
-
# The only available option
|
940
|
-
media_type = media_types[0]
|
941
|
-
else:
|
942
|
-
media_types_repr = ", ".join(media_types)
|
943
|
-
raise UsageError(
|
944
|
-
"Can not detect appropriate media type. "
|
945
|
-
"You can either specify one of the defined media types "
|
946
|
-
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
947
|
-
)
|
948
|
-
return case_cls(
|
1040
|
+
media_type = operation._get_default_media_type()
|
1041
|
+
return Case(
|
949
1042
|
operation=operation,
|
1043
|
+
method=method or operation.method.upper(),
|
1044
|
+
path=path or operation.path,
|
950
1045
|
path_parameters=path_parameters,
|
951
1046
|
headers=CaseInsensitiveDict(headers) if headers is not None else headers,
|
952
1047
|
cookies=cookies,
|
953
1048
|
query=query,
|
954
1049
|
body=body,
|
955
1050
|
media_type=media_type,
|
1051
|
+
meta=meta,
|
956
1052
|
)
|
957
1053
|
|
958
1054
|
def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
|
@@ -970,6 +1066,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
970
1066
|
|
971
1067
|
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
972
1068
|
for parameter in definition.get("parameters", []):
|
1069
|
+
if "$ref" in parameter:
|
1070
|
+
_, parameter = self.resolver.resolve(parameter["$ref"])
|
973
1071
|
if parameter["in"] == "body":
|
974
1072
|
return parameter["schema"]
|
975
1073
|
return None
|
@@ -985,15 +1083,12 @@ class OpenApi30(SwaggerV20):
|
|
985
1083
|
links_field = "links"
|
986
1084
|
|
987
1085
|
@property
|
988
|
-
def
|
989
|
-
|
990
|
-
|
991
|
-
@property
|
992
|
-
def verbose_name(self) -> str:
|
993
|
-
return f"Open API {self.spec_version}"
|
1086
|
+
def specification(self) -> Specification:
|
1087
|
+
version = self.raw_schema["openapi"]
|
1088
|
+
return Specification.openapi(version=version)
|
994
1089
|
|
995
1090
|
def _validate(self) -> None:
|
996
|
-
if self.
|
1091
|
+
if self.specification.version.startswith("3.1"):
|
997
1092
|
# Currently we treat Open API 3.1 as 3.0 in some regard
|
998
1093
|
OPENAPI_31_VALIDATOR.validate(self.raw_schema)
|
999
1094
|
else:
|
@@ -1012,7 +1107,12 @@ class OpenApi30(SwaggerV20):
|
|
1012
1107
|
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
1013
1108
|
) -> list[OpenAPIParameter]:
|
1014
1109
|
# Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
|
1015
|
-
collected: list[OpenAPIParameter] = [
|
1110
|
+
collected: list[OpenAPIParameter] = []
|
1111
|
+
|
1112
|
+
for parameter in parameters:
|
1113
|
+
if parameter["in"] in ("header", "cookie"):
|
1114
|
+
check_header(parameter)
|
1115
|
+
collected.append(OpenAPI30Parameter(definition=parameter))
|
1016
1116
|
if "requestBody" in definition:
|
1017
1117
|
required = definition["requestBody"].get("required", False)
|
1018
1118
|
description = definition["requestBody"].get("description")
|
@@ -1023,34 +1123,38 @@ class OpenApi30(SwaggerV20):
|
|
1023
1123
|
return collected
|
1024
1124
|
|
1025
1125
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
1026
|
-
scopes, definition = self.resolver.resolve_in_scope(
|
1126
|
+
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
1027
1127
|
options = iter(definition.get("content", {}).values())
|
1028
1128
|
option = next(options, None)
|
1029
1129
|
# "schema" is an optional key in the `MediaType` object
|
1030
1130
|
if option and "schema" in option:
|
1031
1131
|
# Extra conversion to JSON Schema is needed here if there was one $ref in the input
|
1032
1132
|
# because it is not converted
|
1033
|
-
return scopes, to_json_schema_recursive(
|
1133
|
+
return scopes, to_json_schema_recursive(
|
1134
|
+
option["schema"], self.nullable_name, is_response_schema=True, update_quantifiers=False
|
1135
|
+
)
|
1034
1136
|
return scopes, None
|
1035
1137
|
|
1036
|
-
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
1138
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
1037
1139
|
"""Get examples from the API operation."""
|
1038
|
-
return get_strategies_from_examples(operation,
|
1140
|
+
return get_strategies_from_examples(operation, **kwargs)
|
1039
1141
|
|
1040
|
-
def get_content_types(self, operation: APIOperation, response:
|
1041
|
-
|
1042
|
-
if not
|
1142
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
1143
|
+
resolved = self._get_response_definitions(operation, response)
|
1144
|
+
if not resolved:
|
1043
1145
|
return []
|
1146
|
+
_, definitions = resolved
|
1044
1147
|
return list(definitions.get("content", {}).keys())
|
1045
1148
|
|
1046
1149
|
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
1047
1150
|
return serialization.serialize_openapi3_parameters(definitions)
|
1048
1151
|
|
1049
1152
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
1050
|
-
|
1153
|
+
request_body = self._resolve_until_no_references(operation.definition.raw["requestBody"])
|
1154
|
+
return list(request_body["content"])
|
1051
1155
|
|
1052
1156
|
def prepare_multipart(
|
1053
|
-
self, form_data:
|
1157
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
1054
1158
|
) -> tuple[list | None, dict[str, Any] | None]:
|
1055
1159
|
"""Prepare form data for sending with `requests`.
|
1056
1160
|
|
@@ -1059,17 +1163,22 @@ class OpenApi30(SwaggerV20):
|
|
1059
1163
|
:return: `files` and `data` values for `requests.request`.
|
1060
1164
|
"""
|
1061
1165
|
files = []
|
1062
|
-
|
1166
|
+
definition = operation.definition.raw
|
1167
|
+
if "$ref" in definition["requestBody"]:
|
1168
|
+
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
1169
|
+
else:
|
1170
|
+
body = definition["requestBody"]
|
1171
|
+
content = body["content"]
|
1063
1172
|
# Open API 3.0 requires media types to be present. We can get here only if the schema defines
|
1064
1173
|
# the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
|
1065
1174
|
for media_type, entry in content.items():
|
1066
|
-
main, sub =
|
1067
|
-
if main in ("*", "multipart") and sub in ("*", "form-data"):
|
1068
|
-
schema = entry
|
1175
|
+
main, sub = media_types.parse(media_type)
|
1176
|
+
if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
|
1177
|
+
schema = entry.get("schema")
|
1069
1178
|
break
|
1070
1179
|
else:
|
1071
1180
|
raise InternalError("No 'multipart/form-data' media type found in the schema")
|
1072
|
-
for name, property_schema in schema.get("properties", {}).items():
|
1181
|
+
for name, property_schema in (schema or {}).get("properties", {}).items():
|
1073
1182
|
if name in form_data:
|
1074
1183
|
if isinstance(form_data[name], list):
|
1075
1184
|
files.extend([(name, item) for item in form_data[name]])
|
@@ -1087,8 +1196,8 @@ class OpenApi30(SwaggerV20):
|
|
1087
1196
|
else:
|
1088
1197
|
body = definition["requestBody"]
|
1089
1198
|
if "content" in body:
|
1090
|
-
main, sub =
|
1199
|
+
main, sub = media_types.parse(media_type)
|
1091
1200
|
for defined_media_type, item in body["content"].items():
|
1092
|
-
if
|
1201
|
+
if media_types.parse(defined_media_type) == (main, sub):
|
1093
1202
|
return item["schema"]
|
1094
1203
|
return None
|