schemathesis 4.1.4__py3-none-any.whl → 4.2.1__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/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +109 -137
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +79 -2
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,67 +1,57 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import itertools
|
4
3
|
import string
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from dataclasses import dataclass, field
|
4
|
+
from contextlib import contextmanager, suppress
|
5
|
+
from dataclasses import dataclass
|
8
6
|
from difflib import get_close_matches
|
9
|
-
from
|
7
|
+
from functools import cached_property, lru_cache
|
10
8
|
from json import JSONDecodeError
|
11
|
-
from threading import RLock
|
12
9
|
from types import SimpleNamespace
|
13
10
|
from typing import (
|
14
11
|
TYPE_CHECKING,
|
15
12
|
Any,
|
16
13
|
Callable,
|
17
|
-
ClassVar,
|
18
14
|
Generator,
|
19
|
-
Iterable,
|
20
15
|
Iterator,
|
21
16
|
Mapping,
|
22
17
|
NoReturn,
|
18
|
+
Sequence,
|
23
19
|
cast,
|
24
20
|
)
|
25
21
|
from urllib.parse import urlsplit
|
26
22
|
|
27
23
|
import jsonschema
|
28
24
|
from packaging import version
|
29
|
-
from requests.exceptions import InvalidHeader
|
30
25
|
from requests.structures import CaseInsensitiveDict
|
31
|
-
from requests.utils import check_header_validity
|
32
26
|
|
33
27
|
from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, Specification, deserialization, media_types
|
28
|
+
from schemathesis.core.adapter import OperationParameter, ResponsesContainer
|
34
29
|
from schemathesis.core.compat import RefResolutionError
|
35
|
-
from schemathesis.core.errors import
|
30
|
+
from schemathesis.core.errors import InfiniteRecursiveReference, InvalidSchema, OperationNotFound
|
36
31
|
from schemathesis.core.failures import Failure, FailureGroup, MalformedJson
|
37
32
|
from schemathesis.core.result import Err, Ok, Result
|
38
|
-
from schemathesis.core.transforms import UNRESOLVABLE, deepclone, resolve_pointer, transform
|
39
33
|
from schemathesis.core.transport import Response
|
40
|
-
from schemathesis.core.validation import INVALID_HEADER_RE
|
41
34
|
from schemathesis.generation.case import Case
|
42
35
|
from schemathesis.generation.meta import CaseMetadata
|
43
36
|
from schemathesis.openapi.checks import JsonSchemaError, MissingContentType
|
44
|
-
from schemathesis.specs.openapi
|
45
|
-
from schemathesis.specs.openapi.
|
37
|
+
from schemathesis.specs.openapi import adapter
|
38
|
+
from schemathesis.specs.openapi.adapter import OpenApiResponses
|
39
|
+
from schemathesis.specs.openapi.adapter.parameters import (
|
40
|
+
COMBINED_FORM_DATA_MARKER,
|
41
|
+
OpenApiParameter,
|
42
|
+
OpenApiParameterSet,
|
43
|
+
)
|
44
|
+
from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
|
45
|
+
from schemathesis.specs.openapi.adapter.security import OpenApiSecurityParameters
|
46
46
|
|
47
47
|
from ...generation import GenerationMode
|
48
48
|
from ...hooks import HookContext, HookDispatcher
|
49
49
|
from ...schemas import APIOperation, APIOperationMap, ApiStatistic, BaseSchema, OperationDefinition
|
50
50
|
from . import serialization
|
51
51
|
from ._hypothesis import openapi_cases
|
52
|
-
from .converter import to_json_schema, to_json_schema_recursive
|
53
52
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
54
53
|
from .examples import get_strategies_from_examples
|
55
|
-
from .
|
56
|
-
OpenAPI20Body,
|
57
|
-
OpenAPI20CompositeBody,
|
58
|
-
OpenAPI20Parameter,
|
59
|
-
OpenAPI30Body,
|
60
|
-
OpenAPI30Parameter,
|
61
|
-
OpenAPIParameter,
|
62
|
-
)
|
63
|
-
from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver
|
64
|
-
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
54
|
+
from .references import ReferenceResolver
|
65
55
|
from .stateful import create_state_machine
|
66
56
|
|
67
57
|
if TYPE_CHECKING:
|
@@ -72,24 +62,10 @@ if TYPE_CHECKING:
|
|
72
62
|
|
73
63
|
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
74
64
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
75
|
-
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, RefResolutionError, InvalidSchema)
|
76
|
-
|
77
|
-
|
78
|
-
def check_header(parameter: dict[str, Any]) -> None:
|
79
|
-
name = parameter["name"]
|
80
|
-
if not name:
|
81
|
-
raise InvalidSchema("Header name should not be empty")
|
82
|
-
if not name.isascii():
|
83
|
-
# `urllib3` encodes header names to ASCII
|
84
|
-
raise InvalidSchema(f"Header name should be ASCII: {name}")
|
85
|
-
try:
|
86
|
-
check_header_validity((name, ""))
|
87
|
-
except InvalidHeader as exc:
|
88
|
-
raise InvalidSchema(str(exc)) from None
|
89
|
-
if bool(INVALID_HEADER_RE.search(name)):
|
90
|
-
raise InvalidSchema(f"Invalid header name: {name}")
|
65
|
+
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, RefResolutionError, InvalidSchema, InfiniteRecursiveReference)
|
91
66
|
|
92
67
|
|
68
|
+
@lru_cache()
|
93
69
|
def get_template_fields(template: str) -> set[str]:
|
94
70
|
"""Extract named placeholders from a string template."""
|
95
71
|
try:
|
@@ -103,16 +79,7 @@ def get_template_fields(template: str) -> set[str]:
|
|
103
79
|
|
104
80
|
@dataclass(eq=False, repr=False)
|
105
81
|
class BaseOpenAPISchema(BaseSchema):
|
106
|
-
|
107
|
-
links_field: ClassVar[str] = ""
|
108
|
-
header_required_field: ClassVar[str] = ""
|
109
|
-
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
110
|
-
_inline_reference_cache: dict[str, Any] = field(default_factory=dict)
|
111
|
-
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
112
|
-
# excessive resolving
|
113
|
-
_inline_reference_cache_lock: RLock = field(default_factory=RLock)
|
114
|
-
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
115
|
-
_path_parameter_template: ClassVar[dict[str, Any]] = None # type: ignore
|
82
|
+
adapter: SpecificationAdapter = None # type: ignore
|
116
83
|
|
117
84
|
@property
|
118
85
|
def specification(self) -> Specification:
|
@@ -125,6 +92,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
125
92
|
def __iter__(self) -> Iterator[str]:
|
126
93
|
return iter(self.raw_schema.get("paths", {}))
|
127
94
|
|
95
|
+
@cached_property
|
96
|
+
def default_media_types(self) -> list[str]:
|
97
|
+
raise NotImplementedError
|
98
|
+
|
128
99
|
def _get_operation_map(self, path: str) -> APIOperationMap:
|
129
100
|
path_item = self.raw_schema.get("paths", {})[path]
|
130
101
|
with in_scope(self.resolver, self.location or ""):
|
@@ -158,8 +129,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
158
129
|
method="",
|
159
130
|
path="",
|
160
131
|
label="",
|
161
|
-
definition=OperationDefinition(raw=None
|
132
|
+
definition=OperationDefinition(raw=None),
|
162
133
|
schema=None, # type: ignore
|
134
|
+
responses=None, # type: ignore
|
135
|
+
security=None, # type: ignore
|
163
136
|
)
|
164
137
|
),
|
165
138
|
) -> bool:
|
@@ -173,7 +146,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
173
146
|
operation.path = path
|
174
147
|
operation.label = f"{method.upper()} {path}"
|
175
148
|
operation.definition.raw = definition
|
176
|
-
operation.definition.resolved = definition
|
177
149
|
operation.schema = self
|
178
150
|
return not self.filter_set.match(_ctx_cache)
|
179
151
|
|
@@ -187,7 +159,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
187
159
|
resolve = self.resolver.resolve
|
188
160
|
resolve_path_item = self._resolve_path_item
|
189
161
|
should_skip = self._should_skip
|
190
|
-
|
162
|
+
links_keyword = self.adapter.links_keyword
|
191
163
|
|
192
164
|
# For operationId lookup
|
193
165
|
selected_operations_by_id: set[str] = set()
|
@@ -214,7 +186,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
214
186
|
for response in definition.get("responses", {}).values():
|
215
187
|
if "$ref" in response:
|
216
188
|
_, response = resolve(response["$ref"])
|
217
|
-
defined_links = response.get(
|
189
|
+
defined_links = response.get(links_keyword)
|
218
190
|
if defined_links is not None:
|
219
191
|
statistic.links.total += len(defined_links)
|
220
192
|
if is_selected:
|
@@ -263,24 +235,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
263
235
|
except SCHEMA_PARSING_ERRORS:
|
264
236
|
continue
|
265
237
|
|
266
|
-
def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
|
267
|
-
while "$ref" in value:
|
268
|
-
_, value = self.resolver.resolve(value["$ref"])
|
269
|
-
return value
|
270
|
-
|
271
|
-
def _resolve_shared_parameters(self, path_item: Mapping[str, Any]) -> list[dict[str, Any]]:
|
272
|
-
return self.resolver.resolve_all(path_item.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
|
273
|
-
|
274
|
-
def _resolve_operation(self, operation: dict[str, Any]) -> dict[str, Any]:
|
275
|
-
return self.resolver.resolve_all(operation, RECURSION_DEPTH_LIMIT - 8)
|
276
|
-
|
277
|
-
def _collect_operation_parameters(
|
278
|
-
self, path_item: Mapping[str, Any], operation: dict[str, Any]
|
279
|
-
) -> list[OpenAPIParameter]:
|
280
|
-
shared_parameters = self._resolve_shared_parameters(path_item)
|
281
|
-
parameters = operation.get("parameters", ())
|
282
|
-
return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
|
283
|
-
|
284
238
|
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
285
239
|
"""Iterate over all operations defined in the API.
|
286
240
|
|
@@ -311,10 +265,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
311
265
|
# Optimization: local variables are faster than attribute access
|
312
266
|
dispatch_hook = self.dispatch_hook
|
313
267
|
resolve_path_item = self._resolve_path_item
|
314
|
-
resolve_shared_parameters = self._resolve_shared_parameters
|
315
|
-
resolve_operation = self._resolve_operation
|
316
268
|
should_skip = self._should_skip
|
317
|
-
|
269
|
+
iter_parameters = self._iter_parameters
|
318
270
|
make_operation = self.make_operation
|
319
271
|
for path, path_item in paths.items():
|
320
272
|
method = None
|
@@ -322,22 +274,19 @@ class BaseOpenAPISchema(BaseSchema):
|
|
322
274
|
dispatch_hook("before_process_path", context, path, path_item)
|
323
275
|
scope, path_item = resolve_path_item(path_item)
|
324
276
|
with in_scope(self.resolver, scope):
|
325
|
-
shared_parameters =
|
277
|
+
shared_parameters = path_item.get("parameters", [])
|
326
278
|
for method, entry in path_item.items():
|
327
279
|
if method not in HTTP_METHODS:
|
328
280
|
continue
|
329
281
|
try:
|
330
|
-
|
331
|
-
if should_skip(path, method, resolved):
|
282
|
+
if should_skip(path, method, entry):
|
332
283
|
continue
|
333
|
-
parameters =
|
334
|
-
parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
|
284
|
+
parameters = iter_parameters(entry, shared_parameters)
|
335
285
|
operation = make_operation(
|
336
286
|
path,
|
337
287
|
method,
|
338
288
|
parameters,
|
339
289
|
entry,
|
340
|
-
resolved,
|
341
290
|
scope,
|
342
291
|
)
|
343
292
|
yield Ok(operation)
|
@@ -360,6 +309,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
360
309
|
method: str | None = None,
|
361
310
|
) -> NoReturn:
|
362
311
|
__tracebackhide__ = True
|
312
|
+
if isinstance(error, InfiniteRecursiveReference):
|
313
|
+
raise InvalidSchema(str(error), path=path, method=method) from None
|
363
314
|
if isinstance(error, RefResolutionError):
|
364
315
|
raise InvalidSchema.from_reference_resolution_error(error, path=path, method=method) from None
|
365
316
|
try:
|
@@ -377,15 +328,28 @@ class BaseOpenAPISchema(BaseSchema):
|
|
377
328
|
def _validate(self) -> None:
|
378
329
|
raise NotImplementedError
|
379
330
|
|
380
|
-
def
|
381
|
-
self,
|
382
|
-
) -> list[
|
383
|
-
|
331
|
+
def _iter_parameters(
|
332
|
+
self, definition: dict[str, Any], shared_parameters: Sequence[dict[str, Any]]
|
333
|
+
) -> list[OperationParameter]:
|
334
|
+
return list(
|
335
|
+
self.adapter.iter_parameters(
|
336
|
+
definition, shared_parameters, self.default_media_types, self.resolver, self.adapter
|
337
|
+
)
|
338
|
+
)
|
384
339
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
340
|
+
def _parse_responses(self, definition: dict[str, Any], scope: str) -> OpenApiResponses:
|
341
|
+
responses = definition.get("responses", {})
|
342
|
+
return OpenApiResponses.from_definition(
|
343
|
+
definition=responses, resolver=self.resolver, scope=scope, adapter=self.adapter
|
344
|
+
)
|
345
|
+
|
346
|
+
def _parse_security(self, definition: dict[str, Any]) -> OpenApiSecurityParameters:
|
347
|
+
return OpenApiSecurityParameters.from_definition(
|
348
|
+
schema=self.raw_schema,
|
349
|
+
operation=definition,
|
350
|
+
resolver=self.resolver,
|
351
|
+
adapter=self.adapter,
|
352
|
+
)
|
389
353
|
|
390
354
|
def _resolve_path_item(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
391
355
|
# The path item could be behind a reference
|
@@ -399,20 +363,27 @@ class BaseOpenAPISchema(BaseSchema):
|
|
399
363
|
self,
|
400
364
|
path: str,
|
401
365
|
method: str,
|
402
|
-
parameters: list[
|
403
|
-
|
404
|
-
resolved: dict[str, Any],
|
366
|
+
parameters: list[OperationParameter],
|
367
|
+
definition: dict[str, Any],
|
405
368
|
scope: str,
|
406
369
|
) -> APIOperation:
|
407
370
|
__tracebackhide__ = True
|
408
371
|
base_url = self.get_base_url()
|
409
|
-
|
372
|
+
responses = self._parse_responses(definition, scope)
|
373
|
+
security = self._parse_security(definition)
|
374
|
+
operation: APIOperation[OperationParameter, ResponsesContainer, OpenApiSecurityParameters] = APIOperation(
|
410
375
|
path=path,
|
411
376
|
method=method,
|
412
|
-
definition=OperationDefinition(
|
377
|
+
definition=OperationDefinition(definition),
|
413
378
|
base_url=base_url,
|
414
379
|
app=self.app,
|
415
380
|
schema=self,
|
381
|
+
responses=responses,
|
382
|
+
security=security,
|
383
|
+
path_parameters=OpenApiParameterSet(),
|
384
|
+
query=OpenApiParameterSet(),
|
385
|
+
headers=OpenApiParameterSet(),
|
386
|
+
cookies=OpenApiParameterSet(),
|
416
387
|
)
|
417
388
|
for parameter in parameters:
|
418
389
|
operation.add_parameter(parameter)
|
@@ -421,19 +392,28 @@ class BaseOpenAPISchema(BaseSchema):
|
|
421
392
|
parameter.name for parameter in operation.path_parameters
|
422
393
|
}
|
423
394
|
for name in missing_parameter_names:
|
424
|
-
|
425
|
-
|
426
|
-
|
395
|
+
operation.add_parameter(
|
396
|
+
self.adapter.build_path_parameter({"name": name, INJECTED_PATH_PARAMETER_KEY: True})
|
397
|
+
)
|
427
398
|
config = self.config.generation_for(operation=operation)
|
428
399
|
if config.with_security_parameters:
|
429
|
-
|
400
|
+
for param in operation.security.iter_parameters():
|
401
|
+
param_name = param.get("name")
|
402
|
+
param_location = param.get("in")
|
403
|
+
if (
|
404
|
+
param_name is not None
|
405
|
+
and param_location is not None
|
406
|
+
and operation.get_parameter(name=param_name, location=param_location) is not None
|
407
|
+
):
|
408
|
+
continue
|
409
|
+
operation.add_parameter(OpenApiParameter.from_definition(definition=param, adapter=self.adapter))
|
430
410
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
431
411
|
return operation
|
432
412
|
|
433
413
|
@property
|
434
|
-
def resolver(self) ->
|
414
|
+
def resolver(self) -> ReferenceResolver:
|
435
415
|
if not hasattr(self, "_resolver"):
|
436
|
-
self._resolver =
|
416
|
+
self._resolver = ReferenceResolver(self.location or "", self.raw_schema)
|
437
417
|
return self._resolver
|
438
418
|
|
439
419
|
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
@@ -444,14 +424,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
444
424
|
"""Get examples from the API operation."""
|
445
425
|
raise NotImplementedError
|
446
426
|
|
447
|
-
def get_security_requirements(self, operation: APIOperation) -> list[str]:
|
448
|
-
"""Get applied security requirements for the given API operation."""
|
449
|
-
return self.security.get_security_requirements(self.raw_schema, operation)
|
450
|
-
|
451
|
-
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
452
|
-
"""Extract response schema from `responses`."""
|
453
|
-
raise NotImplementedError
|
454
|
-
|
455
427
|
def get_operation_by_id(self, operation_id: str) -> APIOperation:
|
456
428
|
"""Get an `APIOperation` instance by its `operationId`."""
|
457
429
|
resolve = self.resolver.resolve
|
@@ -467,9 +439,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
467
439
|
if method not in HTTP_METHODS:
|
468
440
|
continue
|
469
441
|
if "operationId" in operation and operation["operationId"] == operation_id:
|
470
|
-
|
471
|
-
|
472
|
-
return self.make_operation(path, method, parameters, operation, resolved, scope)
|
442
|
+
parameters = self._iter_parameters(operation, path_item.get("parameters", []))
|
443
|
+
return self.make_operation(path, method, parameters, operation, scope)
|
473
444
|
self._on_missing_operation(operation_id, None, [])
|
474
445
|
|
475
446
|
def get_operation_by_reference(self, reference: str) -> APIOperation:
|
@@ -480,12 +451,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
480
451
|
scope, operation = self.resolver.resolve(reference)
|
481
452
|
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
482
453
|
path = path.replace("~1", "/").replace("~0", "~")
|
483
|
-
with in_scope(self.resolver, scope):
|
484
|
-
resolved = self._resolve_operation(operation)
|
485
454
|
parent_ref, _ = reference.rsplit("/", maxsplit=1)
|
486
455
|
_, path_item = self.resolver.resolve(parent_ref)
|
487
|
-
|
488
|
-
|
456
|
+
with in_scope(self.resolver, scope):
|
457
|
+
parameters = self._iter_parameters(operation, path_item.get("parameters", []))
|
458
|
+
return self.make_operation(path, method, parameters, operation, scope)
|
489
459
|
|
490
460
|
def get_case_strategy(
|
491
461
|
self,
|
@@ -507,10 +477,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
507
477
|
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
508
478
|
config = self.config.generation_for(operation=operation)
|
509
479
|
if config.with_security_parameters:
|
510
|
-
security_parameters =
|
511
|
-
self.raw_schema, operation, self.resolver, location
|
512
|
-
)
|
513
|
-
security_parameters = [item for item in security_parameters if item["in"] == location]
|
480
|
+
security_parameters = [param for param in operation.security.iter_parameters() if param["in"] == location]
|
514
481
|
if security_parameters:
|
515
482
|
definitions.extend(security_parameters)
|
516
483
|
if definitions:
|
@@ -520,64 +487,22 @@ class BaseOpenAPISchema(BaseSchema):
|
|
520
487
|
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
521
488
|
raise NotImplementedError
|
522
489
|
|
523
|
-
def _get_response_definitions(
|
524
|
-
self, operation: APIOperation, response: Response
|
525
|
-
) -> tuple[list[str], dict[str, Any]] | None:
|
526
|
-
try:
|
527
|
-
responses = operation.definition.raw["responses"]
|
528
|
-
except KeyError as exc:
|
529
|
-
path = operation.path
|
530
|
-
self._raise_invalid_schema(exc, path, operation.method)
|
531
|
-
definition = _get_response_definition_by_status(response.status_code, responses)
|
532
|
-
if definition is None:
|
533
|
-
return None
|
534
|
-
return self.resolver.resolve_in_scope(definition, operation.definition.scope)
|
535
|
-
|
536
|
-
def get_headers(
|
537
|
-
self, operation: APIOperation, response: Response
|
538
|
-
) -> tuple[list[str], dict[str, dict[str, Any]] | None] | None:
|
539
|
-
resolved = self._get_response_definitions(operation, response)
|
540
|
-
if not resolved:
|
541
|
-
return None
|
542
|
-
scopes, definitions = resolved
|
543
|
-
return scopes, definitions.get("headers")
|
544
|
-
|
545
490
|
def as_state_machine(self) -> type[APIStateMachine]:
|
546
491
|
return create_state_machine(self)
|
547
492
|
|
548
|
-
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
549
|
-
result: dict[str, dict[str, Any]] = defaultdict(dict)
|
550
|
-
for status_code, link in links.get_all_links(operation):
|
551
|
-
if isinstance(link, Ok):
|
552
|
-
name = link.ok().name
|
553
|
-
else:
|
554
|
-
name = link.err().name
|
555
|
-
result[status_code][name] = link
|
556
|
-
|
557
|
-
return result
|
558
|
-
|
559
493
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
560
494
|
return operation.definition.raw.get("tags")
|
561
495
|
|
562
|
-
@property
|
563
|
-
def validator_cls(self) -> type[jsonschema.Validator]:
|
564
|
-
if self.specification.version.startswith("3.1"):
|
565
|
-
return jsonschema.Draft202012Validator
|
566
|
-
return jsonschema.Draft4Validator
|
567
|
-
|
568
496
|
def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
|
569
497
|
__tracebackhide__ = True
|
570
|
-
|
571
|
-
definition
|
572
|
-
|
573
|
-
# No response defined for the received response status code
|
574
|
-
return None
|
575
|
-
scopes, schema = self.get_response_schema(definition, operation.definition.scope)
|
576
|
-
if not schema:
|
577
|
-
# No schema to check against
|
498
|
+
definition = operation.responses.find_by_status_code(response.status_code)
|
499
|
+
if definition is None or definition.schema is None:
|
500
|
+
# No definition for the given HTTP response, or missing "schema" in the matching definition
|
578
501
|
return None
|
579
|
-
|
502
|
+
|
580
503
|
failures: list[Failure] = []
|
504
|
+
|
505
|
+
content_types = response.headers.get("content-type")
|
581
506
|
if content_types is None:
|
582
507
|
all_media_types = self.get_content_types(operation, response)
|
583
508
|
formatted_content_types = [f"\n- `{content_type}`" for content_type in all_media_types]
|
@@ -587,6 +512,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
587
512
|
content_type = "application/json"
|
588
513
|
else:
|
589
514
|
content_type = content_types[0]
|
515
|
+
|
590
516
|
try:
|
591
517
|
data = deserialization.deserialize_response(response, content_type)
|
592
518
|
except JSONDecodeError as exc:
|
@@ -607,146 +533,24 @@ class BaseOpenAPISchema(BaseSchema):
|
|
607
533
|
)
|
608
534
|
_maybe_raise_one_or_more(failures)
|
609
535
|
return None
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
) from exc
|
624
|
-
except jsonschema.ValidationError as exc:
|
625
|
-
failures.append(
|
626
|
-
JsonSchemaError.from_exception(
|
627
|
-
operation=operation.label,
|
628
|
-
exc=exc,
|
629
|
-
config=operation.schema.config.output,
|
630
|
-
)
|
536
|
+
|
537
|
+
try:
|
538
|
+
definition.validator.validate(data)
|
539
|
+
except jsonschema.SchemaError as exc:
|
540
|
+
raise InvalidSchema.from_jsonschema_error(
|
541
|
+
exc, path=operation.path, method=operation.method, config=self.config.output
|
542
|
+
) from exc
|
543
|
+
except jsonschema.ValidationError as exc:
|
544
|
+
failures.append(
|
545
|
+
JsonSchemaError.from_exception(
|
546
|
+
operation=operation.label,
|
547
|
+
exc=exc,
|
548
|
+
config=operation.schema.config.output,
|
631
549
|
)
|
550
|
+
)
|
632
551
|
_maybe_raise_one_or_more(failures)
|
633
552
|
return None # explicitly return None for mypy
|
634
553
|
|
635
|
-
@contextmanager
|
636
|
-
def _validating_response(self, scopes: list[str]) -> Generator[ConvertingResolver, None, None]:
|
637
|
-
resolver = ConvertingResolver(
|
638
|
-
self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
|
639
|
-
)
|
640
|
-
with in_scopes(resolver, scopes):
|
641
|
-
yield resolver
|
642
|
-
|
643
|
-
@property
|
644
|
-
def rewritten_components(self) -> dict[str, Any]:
|
645
|
-
if not hasattr(self, "_rewritten_components"):
|
646
|
-
|
647
|
-
def callback(_schema: dict[str, Any], nullable_name: str) -> dict[str, Any]:
|
648
|
-
_schema = to_json_schema(_schema, nullable_name=nullable_name, copy=False)
|
649
|
-
return self._rewrite_references(_schema, self.resolver)
|
650
|
-
|
651
|
-
# Different spec versions allow different keywords to store possible reference targets
|
652
|
-
components: dict[str, Any] = {}
|
653
|
-
for path in self.component_locations:
|
654
|
-
schema = self.raw_schema
|
655
|
-
target = components
|
656
|
-
for chunk in path:
|
657
|
-
if chunk in schema:
|
658
|
-
schema = schema[chunk]
|
659
|
-
target = target.setdefault(chunk, {})
|
660
|
-
else:
|
661
|
-
break
|
662
|
-
else:
|
663
|
-
target.update(transform(deepclone(schema), callback, self.nullable_name))
|
664
|
-
if self._inline_reference_cache:
|
665
|
-
components[INLINED_REFERENCES_KEY] = self._inline_reference_cache
|
666
|
-
self._rewritten_components = components
|
667
|
-
return self._rewritten_components
|
668
|
-
|
669
|
-
def prepare_schema(self, schema: Any) -> Any:
|
670
|
-
"""Inline Open API definitions.
|
671
|
-
|
672
|
-
Inlining components helps `hypothesis-jsonschema` generate data that involves non-resolved references.
|
673
|
-
"""
|
674
|
-
schema = deepclone(schema)
|
675
|
-
schema = transform(schema, self._rewrite_references, self.resolver)
|
676
|
-
# Only add definitions that are reachable from the schema via references
|
677
|
-
stack = [schema]
|
678
|
-
seen = set()
|
679
|
-
while stack:
|
680
|
-
item = stack.pop()
|
681
|
-
if isinstance(item, dict):
|
682
|
-
if "$ref" in item:
|
683
|
-
reference = item["$ref"]
|
684
|
-
if isinstance(reference, str) and reference.startswith("#/") and reference not in seen:
|
685
|
-
seen.add(reference)
|
686
|
-
# Resolve the component and add it to the proper place in the schema
|
687
|
-
pointer = reference[1:]
|
688
|
-
resolved = resolve_pointer(self.rewritten_components, pointer)
|
689
|
-
if resolved is UNRESOLVABLE:
|
690
|
-
raise LoaderError(
|
691
|
-
LoaderErrorKind.OPEN_API_INVALID_SCHEMA,
|
692
|
-
message=f"Unresolvable JSON pointer in the schema: {pointer}",
|
693
|
-
)
|
694
|
-
if isinstance(resolved, dict):
|
695
|
-
container = schema
|
696
|
-
for key in pointer.split("/")[1:]:
|
697
|
-
container = container.setdefault(key, {})
|
698
|
-
container.update(resolved)
|
699
|
-
# Explore the resolved value too
|
700
|
-
stack.append(resolved)
|
701
|
-
# Still explore other values as they may have nested references in other keys
|
702
|
-
for value in item.values():
|
703
|
-
if isinstance(value, (dict, list)):
|
704
|
-
stack.append(value)
|
705
|
-
elif isinstance(item, list):
|
706
|
-
for sub_item in item:
|
707
|
-
if isinstance(sub_item, (dict, list)):
|
708
|
-
stack.append(sub_item)
|
709
|
-
return schema
|
710
|
-
|
711
|
-
def _rewrite_references(self, schema: dict[str, Any], resolver: InliningResolver) -> dict[str, Any]:
|
712
|
-
"""Rewrite references present in the schema.
|
713
|
-
|
714
|
-
The idea is to resolve references, cache the result and replace these references with new ones
|
715
|
-
that point to a local path which is populated from this cache later on.
|
716
|
-
"""
|
717
|
-
reference = schema.get("$ref")
|
718
|
-
# If `$ref` is not a property name and should be processed
|
719
|
-
if reference is not None and isinstance(reference, str) and not reference.startswith("#/"):
|
720
|
-
key = _make_reference_key(resolver._scopes_stack, reference)
|
721
|
-
with self._inline_reference_cache_lock:
|
722
|
-
if key not in self._inline_reference_cache:
|
723
|
-
with resolver.resolving(reference) as resolved:
|
724
|
-
# Resolved object also may have references
|
725
|
-
self._inline_reference_cache[key] = transform(
|
726
|
-
resolved, lambda s: self._rewrite_references(s, resolver)
|
727
|
-
)
|
728
|
-
# Rewrite the reference with the new location
|
729
|
-
schema["$ref"] = f"#/{INLINED_REFERENCES_KEY}/{key}"
|
730
|
-
return schema
|
731
|
-
|
732
|
-
|
733
|
-
def _get_response_definition_by_status(status_code: int, responses: dict[str, Any]) -> dict[str, Any] | None:
|
734
|
-
# Cast to string, as integers are often there due to YAML deserialization
|
735
|
-
responses = {str(status): definition for status, definition in responses.items()}
|
736
|
-
if str(status_code) in responses:
|
737
|
-
return responses[str(status_code)]
|
738
|
-
# More specific should go first
|
739
|
-
keys = sorted(responses, key=lambda k: k.count("X"))
|
740
|
-
for key in keys:
|
741
|
-
if key == "default":
|
742
|
-
continue
|
743
|
-
status_codes = expand_status_code(key)
|
744
|
-
if status_code in status_codes:
|
745
|
-
return responses[key]
|
746
|
-
if "default" in responses:
|
747
|
-
return responses["default"]
|
748
|
-
return None
|
749
|
-
|
750
554
|
|
751
555
|
def _maybe_raise_one_or_more(failures: list[Failure]) -> None:
|
752
556
|
if not failures:
|
@@ -756,22 +560,6 @@ def _maybe_raise_one_or_more(failures: list[Failure]) -> None:
|
|
756
560
|
raise FailureGroup(failures) from None
|
757
561
|
|
758
562
|
|
759
|
-
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
760
|
-
"""A name under which the resolved reference data will be stored."""
|
761
|
-
# Using a hexdigest is the simplest way to associate practically unique keys with each reference
|
762
|
-
digest = sha1()
|
763
|
-
for scope in scopes:
|
764
|
-
digest.update(scope.encode("utf-8"))
|
765
|
-
# Separator to avoid collisions like this: ["a"], "bc" vs. ["ab"], "c". Otherwise, the resulting digest
|
766
|
-
# will be the same for both cases
|
767
|
-
digest.update(b"#")
|
768
|
-
digest.update(reference.encode("utf-8"))
|
769
|
-
return digest.hexdigest()
|
770
|
-
|
771
|
-
|
772
|
-
INLINED_REFERENCES_KEY = "x-inlined"
|
773
|
-
|
774
|
-
|
775
563
|
@contextmanager
|
776
564
|
def in_scope(resolver: jsonschema.RefResolver, scope: str) -> Generator[None, None, None]:
|
777
565
|
resolver.push_scope(scope)
|
@@ -781,20 +569,6 @@ def in_scope(resolver: jsonschema.RefResolver, scope: str) -> Generator[None, No
|
|
781
569
|
resolver.pop_scope()
|
782
570
|
|
783
571
|
|
784
|
-
@contextmanager
|
785
|
-
def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[None, None, None]:
|
786
|
-
"""Push all available scopes into the resolver.
|
787
|
-
|
788
|
-
There could be an additional scope change during a schema resolving in `get_response_schema`, so in total there
|
789
|
-
could be a stack of two scopes maximum. This context manager handles both cases (1 or 2 scope changes) in the same
|
790
|
-
way.
|
791
|
-
"""
|
792
|
-
with ExitStack() as stack:
|
793
|
-
for scope in scopes:
|
794
|
-
stack.enter_context(in_scope(resolver, scope))
|
795
|
-
yield
|
796
|
-
|
797
|
-
|
798
572
|
@dataclass
|
799
573
|
class MethodMap(Mapping):
|
800
574
|
"""Container for accessing API operations.
|
@@ -824,13 +598,12 @@ class MethodMap(Mapping):
|
|
824
598
|
schema = cast(BaseOpenAPISchema, self._parent._schema)
|
825
599
|
path = self._path
|
826
600
|
scope = self._scope
|
827
|
-
schema.resolver
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
return schema.make_operation(path, method, parameters, operation, resolved, scope)
|
601
|
+
with in_scope(schema.resolver, scope):
|
602
|
+
try:
|
603
|
+
parameters = schema._iter_parameters(operation, self._path_item.get("parameters", []))
|
604
|
+
except SCHEMA_PARSING_ERRORS as exc:
|
605
|
+
schema._raise_invalid_schema(exc, path, method)
|
606
|
+
return schema.make_operation(path, method, parameters, operation, scope)
|
834
607
|
|
835
608
|
def __getitem__(self, item: str) -> APIOperation:
|
836
609
|
try:
|
@@ -843,86 +616,30 @@ class MethodMap(Mapping):
|
|
843
616
|
raise LookupError(message) from exc
|
844
617
|
|
845
618
|
|
846
|
-
OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
|
847
|
-
OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE = "multipart/form-data"
|
848
|
-
|
849
|
-
|
850
619
|
class SwaggerV20(BaseOpenAPISchema):
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
header_required_field = "x-required"
|
855
|
-
security = SwaggerSecurityProcessor()
|
856
|
-
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = (("definitions",),)
|
857
|
-
links_field = "x-links"
|
858
|
-
_path_parameter_template = {"in": "path", "required": True, "type": "string"}
|
620
|
+
def __post_init__(self) -> None:
|
621
|
+
self.adapter = adapter.v2
|
622
|
+
super().__post_init__()
|
859
623
|
|
860
624
|
@property
|
861
625
|
def specification(self) -> Specification:
|
862
626
|
version = self.raw_schema.get("swagger", "2.0")
|
863
627
|
return Specification.openapi(version=version)
|
864
628
|
|
629
|
+
@cached_property
|
630
|
+
def default_media_types(self) -> list[str]:
|
631
|
+
return self.raw_schema.get("consumes", [])
|
632
|
+
|
865
633
|
def _validate(self) -> None:
|
866
634
|
SWAGGER_20_VALIDATOR.validate(self.raw_schema)
|
867
635
|
|
868
636
|
def _get_base_path(self) -> str:
|
869
637
|
return self.raw_schema.get("basePath", "/")
|
870
638
|
|
871
|
-
def collect_parameters(
|
872
|
-
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
873
|
-
) -> list[OpenAPIParameter]:
|
874
|
-
# The main difference with Open API 3.0 is that it has `body` and `form` parameters that we need to handle
|
875
|
-
# differently.
|
876
|
-
collected: list[OpenAPIParameter] = []
|
877
|
-
# NOTE. The Open API 2.0 spec doesn't strictly imply having media types in the "consumes" keyword.
|
878
|
-
# It is not enforced by the meta schema and has no "MUST" verb in the spec text.
|
879
|
-
# Also, not every API has operations with payload (they might have only GET operations without payloads).
|
880
|
-
# For these reasons, it might be (and often is) absent, and we need to provide the proper media type in case
|
881
|
-
# we have operations with a payload.
|
882
|
-
media_types = self._get_consumes_for_operation(definition)
|
883
|
-
# For `in=body` parameters, we imply `application/json` as the default media type because it is the most common.
|
884
|
-
body_media_types = media_types or (OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE,)
|
885
|
-
# If an API operation has parameters with `in=formData`, Schemathesis should know how to serialize it.
|
886
|
-
# We can't be 100% sure what media type is expected by the server and chose `multipart/form-data` as
|
887
|
-
# the default because it is broader since it allows us to upload files.
|
888
|
-
form_data_media_types = media_types or (OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE,)
|
889
|
-
|
890
|
-
form_parameters = []
|
891
|
-
for parameter in parameters:
|
892
|
-
if parameter["in"] == "formData":
|
893
|
-
# We need to gather form parameters first before creating a composite parameter for them
|
894
|
-
form_parameters.append(parameter)
|
895
|
-
elif parameter["in"] == "body":
|
896
|
-
for media_type in body_media_types:
|
897
|
-
collected.append(OpenAPI20Body(definition=parameter, media_type=media_type))
|
898
|
-
else:
|
899
|
-
if parameter["in"] in ("header", "cookie"):
|
900
|
-
check_header(parameter)
|
901
|
-
collected.append(OpenAPI20Parameter(definition=parameter))
|
902
|
-
|
903
|
-
if form_parameters:
|
904
|
-
for media_type in form_data_media_types:
|
905
|
-
collected.append(
|
906
|
-
# Individual `formData` parameters are joined into a single "composite" one.
|
907
|
-
OpenAPI20CompositeBody.from_parameters(*form_parameters, media_type=media_type)
|
908
|
-
)
|
909
|
-
return collected
|
910
|
-
|
911
639
|
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
912
640
|
"""Get examples from the API operation."""
|
913
641
|
return get_strategies_from_examples(operation, **kwargs)
|
914
642
|
|
915
|
-
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
916
|
-
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
917
|
-
schema = definition.get("schema")
|
918
|
-
if not schema:
|
919
|
-
return scopes, None
|
920
|
-
# Extra conversion to JSON Schema is needed here if there was one $ref in the input
|
921
|
-
# because it is not converted
|
922
|
-
return scopes, to_json_schema_recursive(
|
923
|
-
schema, self.nullable_name, is_response_schema=True, update_quantifiers=False
|
924
|
-
)
|
925
|
-
|
926
643
|
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
927
644
|
produces = operation.definition.raw.get("produces", None)
|
928
645
|
if produces:
|
@@ -944,9 +661,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
944
661
|
known_fields: dict[str, dict] = {}
|
945
662
|
|
946
663
|
for parameter in operation.body:
|
947
|
-
if
|
948
|
-
|
949
|
-
known_fields[form_parameter.name] = form_parameter.definition
|
664
|
+
if COMBINED_FORM_DATA_MARKER in parameter.definition:
|
665
|
+
known_fields.update(parameter.definition["schema"].get("properties", {}))
|
950
666
|
|
951
667
|
def add_file(name: str, value: Any) -> None:
|
952
668
|
if isinstance(value, list):
|
@@ -1007,30 +723,24 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
1007
723
|
consumes = global_consumes
|
1008
724
|
return consumes
|
1009
725
|
|
1010
|
-
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
1011
|
-
for parameter in definition.get("parameters", []):
|
1012
|
-
if "$ref" in parameter:
|
1013
|
-
_, parameter = self.resolver.resolve(parameter["$ref"])
|
1014
|
-
if parameter["in"] == "body":
|
1015
|
-
return parameter["schema"]
|
1016
|
-
return None
|
1017
|
-
|
1018
726
|
|
1019
727
|
class OpenApi30(SwaggerV20):
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
links_field = "links"
|
1027
|
-
_path_parameter_template = {"in": "path", "required": True, "schema": {"type": "string"}}
|
728
|
+
def __post_init__(self) -> None:
|
729
|
+
if self.specification.version.startswith("3.1"):
|
730
|
+
self.adapter = adapter.v3_1
|
731
|
+
else:
|
732
|
+
self.adapter = adapter.v3_0
|
733
|
+
BaseOpenAPISchema.__post_init__(self)
|
1028
734
|
|
1029
735
|
@property
|
1030
736
|
def specification(self) -> Specification:
|
1031
737
|
version = self.raw_schema["openapi"]
|
1032
738
|
return Specification.openapi(version=version)
|
1033
739
|
|
740
|
+
@cached_property
|
741
|
+
def default_media_types(self) -> list[str]:
|
742
|
+
return []
|
743
|
+
|
1034
744
|
def _validate(self) -> None:
|
1035
745
|
if self.specification.version.startswith("3.1"):
|
1036
746
|
# Currently we treat Open API 3.1 as 3.0 in some regard
|
@@ -1047,79 +757,34 @@ class OpenApi30(SwaggerV20):
|
|
1047
757
|
return urlsplit(url).path
|
1048
758
|
return "/"
|
1049
759
|
|
1050
|
-
def collect_parameters(
|
1051
|
-
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
1052
|
-
) -> list[OpenAPIParameter]:
|
1053
|
-
# Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
|
1054
|
-
collected: list[OpenAPIParameter] = []
|
1055
|
-
|
1056
|
-
for parameter in parameters:
|
1057
|
-
if parameter["in"] in ("header", "cookie"):
|
1058
|
-
check_header(parameter)
|
1059
|
-
collected.append(OpenAPI30Parameter(definition=parameter))
|
1060
|
-
if "requestBody" in definition:
|
1061
|
-
required = definition["requestBody"].get("required", False)
|
1062
|
-
description = definition["requestBody"].get("description")
|
1063
|
-
for media_type, content in definition["requestBody"]["content"].items():
|
1064
|
-
collected.append(
|
1065
|
-
OpenAPI30Body(content, description=description, media_type=media_type, required=required)
|
1066
|
-
)
|
1067
|
-
return collected
|
1068
|
-
|
1069
|
-
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
1070
|
-
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
1071
|
-
options = iter(definition.get("content", {}).values())
|
1072
|
-
option = next(options, None)
|
1073
|
-
# "schema" is an optional key in the `MediaType` object
|
1074
|
-
if option and "schema" in option:
|
1075
|
-
# Extra conversion to JSON Schema is needed here if there was one $ref in the input
|
1076
|
-
# because it is not converted
|
1077
|
-
return scopes, to_json_schema_recursive(
|
1078
|
-
option["schema"], self.nullable_name, is_response_schema=True, update_quantifiers=False
|
1079
|
-
)
|
1080
|
-
return scopes, None
|
1081
|
-
|
1082
760
|
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
1083
761
|
"""Get examples from the API operation."""
|
1084
762
|
return get_strategies_from_examples(operation, **kwargs)
|
1085
763
|
|
1086
764
|
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
1087
|
-
|
1088
|
-
if
|
765
|
+
definition = operation.responses.find_by_status_code(response.status_code)
|
766
|
+
if definition is None:
|
1089
767
|
return []
|
1090
|
-
|
1091
|
-
return list(definitions.get("content", {}).keys())
|
768
|
+
return list(definition.definition.get("content", {}).keys())
|
1092
769
|
|
1093
770
|
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
1094
771
|
return serialization.serialize_openapi3_parameters(definitions)
|
1095
772
|
|
1096
773
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
1097
|
-
|
1098
|
-
return list(request_body["content"])
|
774
|
+
return [body.media_type for body in operation.body]
|
1099
775
|
|
1100
776
|
def prepare_multipart(
|
1101
777
|
self, form_data: dict[str, Any], operation: APIOperation
|
1102
778
|
) -> tuple[list | None, dict[str, Any] | None]:
|
1103
779
|
files = []
|
1104
|
-
definition = operation.definition.raw
|
1105
|
-
if "$ref" in definition["requestBody"]:
|
1106
|
-
self.resolver.push_scope(operation.definition.scope)
|
1107
|
-
try:
|
1108
|
-
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
1109
|
-
finally:
|
1110
|
-
self.resolver.pop_scope()
|
1111
|
-
else:
|
1112
|
-
body = definition["requestBody"]
|
1113
|
-
content = body["content"]
|
1114
780
|
# Open API 3.0 requires media types to be present. We can get here only if the schema defines
|
1115
781
|
# the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
|
1116
|
-
|
1117
|
-
|
782
|
+
schema = {}
|
783
|
+
for body in operation.body:
|
784
|
+
main, sub = media_types.parse(body.media_type)
|
1118
785
|
if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
|
1119
|
-
schema =
|
786
|
+
schema = body.definition.get("schema")
|
1120
787
|
break
|
1121
|
-
else:
|
1122
|
-
raise InternalError("No 'multipart/form-data' media type found in the schema")
|
1123
788
|
for name, value in form_data.items():
|
1124
789
|
property_schema = (schema or {}).get("properties", {}).get(name)
|
1125
790
|
if property_schema:
|
@@ -1135,16 +800,3 @@ class OpenApi30(SwaggerV20):
|
|
1135
800
|
files.append((name, (None, value)))
|
1136
801
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
1137
802
|
return files or None, None
|
1138
|
-
|
1139
|
-
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
1140
|
-
if "requestBody" in definition:
|
1141
|
-
if "$ref" in definition["requestBody"]:
|
1142
|
-
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
1143
|
-
else:
|
1144
|
-
body = definition["requestBody"]
|
1145
|
-
if "content" in body:
|
1146
|
-
main, sub = media_types.parse(media_type)
|
1147
|
-
for defined_media_type, item in body["content"].items():
|
1148
|
-
if media_types.parse(defined_media_type) == (main, sub):
|
1149
|
-
return item["schema"]
|
1150
|
-
return None
|