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