schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- 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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -19,43 +19,35 @@ from typing import (
|
|
19
19
|
Iterator,
|
20
20
|
Mapping,
|
21
21
|
NoReturn,
|
22
|
-
Sequence,
|
23
|
-
TypeVar,
|
24
22
|
cast,
|
25
23
|
)
|
26
24
|
from urllib.parse import urlsplit
|
27
25
|
|
28
26
|
import jsonschema
|
29
27
|
from packaging import version
|
28
|
+
from requests.exceptions import InvalidHeader
|
30
29
|
from requests.structures import CaseInsensitiveDict
|
31
|
-
|
32
|
-
|
33
|
-
from
|
34
|
-
from
|
35
|
-
from
|
36
|
-
from
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
from ...generation import
|
47
|
-
from ...hooks import
|
48
|
-
from ...
|
49
|
-
from
|
50
|
-
from ...internal.result import Err, Ok, Result
|
51
|
-
from ...models import APIOperation, Case, OperationDefinition
|
52
|
-
from ...schemas import APIOperationMap, BaseSchema
|
53
|
-
from ...stateful import Stateful, StatefulTest
|
54
|
-
from ...transports.content_types import is_json_media_type, parse_content_type
|
55
|
-
from ...transports.responses import get_json
|
56
|
-
from . import links, serialization
|
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.openapi.checks import JsonSchemaError, MissingContentType
|
43
|
+
from schemathesis.specs.openapi.stateful import links
|
44
|
+
|
45
|
+
from ...generation import GenerationMode
|
46
|
+
from ...hooks import HookContext, HookDispatcher
|
47
|
+
from ...schemas import APIOperation, APIOperationMap, ApiStatistic, BaseSchema, OperationDefinition
|
48
|
+
from . import serialization
|
57
49
|
from ._cache import OperationCache
|
58
|
-
from ._hypothesis import
|
50
|
+
from ._hypothesis import openapi_cases
|
59
51
|
from .converter import to_json_schema, to_json_schema_recursive
|
60
52
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
61
53
|
from .examples import get_strategies_from_examples
|
@@ -67,26 +59,34 @@ from .parameters import (
|
|
67
59
|
OpenAPI30Parameter,
|
68
60
|
OpenAPIParameter,
|
69
61
|
)
|
70
|
-
from .references import
|
71
|
-
RECURSION_DEPTH_LIMIT,
|
72
|
-
UNRESOLVABLE,
|
73
|
-
ConvertingResolver,
|
74
|
-
InliningResolver,
|
75
|
-
resolve_pointer,
|
76
|
-
)
|
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
67
|
from hypothesis.strategies import SearchStrategy
|
82
68
|
|
83
|
-
from
|
84
|
-
from
|
85
|
-
from ...transports.responses import GenericResponse
|
86
|
-
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
69
|
+
from schemathesis.auths import AuthStorage
|
70
|
+
from schemathesis.generation.stateful import APIStateMachine
|
87
71
|
|
72
|
+
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
88
73
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
89
|
-
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}")
|
90
90
|
|
91
91
|
|
92
92
|
@dataclass(eq=False, repr=False)
|
@@ -103,16 +103,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
103
103
|
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
104
104
|
|
105
105
|
@property
|
106
|
-
def
|
106
|
+
def specification(self) -> Specification:
|
107
107
|
raise NotImplementedError
|
108
108
|
|
109
|
-
def get_stateful_tests(
|
110
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
111
|
-
) -> Sequence[StatefulTest]:
|
112
|
-
if stateful == Stateful.links:
|
113
|
-
return links.get_links(response, operation, field=self.links_field)
|
114
|
-
return []
|
115
|
-
|
116
109
|
def __repr__(self) -> str:
|
117
110
|
info = self.raw_schema["info"]
|
118
111
|
return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
|
@@ -126,13 +119,18 @@ class BaseOpenAPISchema(BaseSchema):
|
|
126
119
|
if map is not None:
|
127
120
|
return map
|
128
121
|
path_item = self.raw_schema.get("paths", {})[path]
|
129
|
-
|
122
|
+
with in_scope(self.resolver, self.location or ""):
|
123
|
+
scope, path_item = self._resolve_path_item(path_item)
|
130
124
|
self.dispatch_hook("before_process_path", HookContext(), path, path_item)
|
131
125
|
map = APIOperationMap(self, {})
|
132
126
|
map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
|
133
127
|
cache.insert_map(path, map)
|
134
128
|
return map
|
135
129
|
|
130
|
+
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
131
|
+
method, path = label.split(" ", maxsplit=1)
|
132
|
+
return self[path][method]
|
133
|
+
|
136
134
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
137
135
|
matches = get_close_matches(item, list(self))
|
138
136
|
self._on_missing_operation(item, exc, matches)
|
@@ -152,7 +150,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
152
150
|
operation=APIOperation(
|
153
151
|
method="",
|
154
152
|
path="",
|
155
|
-
|
153
|
+
label="",
|
156
154
|
definition=OperationDefinition(raw=None, resolved=None, scope=""),
|
157
155
|
schema=None, # type: ignore
|
158
156
|
)
|
@@ -162,17 +160,84 @@ class BaseOpenAPISchema(BaseSchema):
|
|
162
160
|
return True
|
163
161
|
if self.filter_set.is_empty():
|
164
162
|
return False
|
165
|
-
path = self.get_full_path(path)
|
166
163
|
# Attribute assignment is way faster than creating a new namespace every time
|
167
164
|
operation = _ctx_cache.operation
|
168
165
|
operation.method = method
|
169
166
|
operation.path = path
|
170
|
-
operation.
|
167
|
+
operation.label = f"{method.upper()} {path}"
|
171
168
|
operation.definition.raw = definition
|
172
169
|
operation.definition.resolved = definition
|
173
170
|
operation.schema = self
|
174
171
|
return not self.filter_set.match(_ctx_cache)
|
175
172
|
|
173
|
+
def _measure_statistic(self) -> ApiStatistic:
|
174
|
+
statistic = ApiStatistic()
|
175
|
+
try:
|
176
|
+
paths = self.raw_schema["paths"]
|
177
|
+
except KeyError:
|
178
|
+
return statistic
|
179
|
+
|
180
|
+
resolve = self.resolver.resolve
|
181
|
+
resolve_path_item = self._resolve_path_item
|
182
|
+
should_skip = self._should_skip
|
183
|
+
links_field = self.links_field
|
184
|
+
|
185
|
+
# For operationId lookup
|
186
|
+
selected_operations_by_id: set[str] = set()
|
187
|
+
# Tuples of (method, path)
|
188
|
+
selected_operations_by_path: set[tuple[str, str]] = set()
|
189
|
+
collected_links: list[dict] = []
|
190
|
+
|
191
|
+
for path, path_item in paths.items():
|
192
|
+
try:
|
193
|
+
scope, path_item = resolve_path_item(path_item)
|
194
|
+
self.resolver.push_scope(scope)
|
195
|
+
try:
|
196
|
+
for method, definition in path_item.items():
|
197
|
+
if method not in HTTP_METHODS:
|
198
|
+
continue
|
199
|
+
statistic.operations.total += 1
|
200
|
+
is_selected = not should_skip(path, method, definition)
|
201
|
+
if is_selected:
|
202
|
+
statistic.operations.selected += 1
|
203
|
+
# Store both identifiers
|
204
|
+
if "operationId" in definition:
|
205
|
+
selected_operations_by_id.add(definition["operationId"])
|
206
|
+
selected_operations_by_path.add((method, path))
|
207
|
+
for response in definition.get("responses", {}).values():
|
208
|
+
if "$ref" in response:
|
209
|
+
_, response = resolve(response["$ref"])
|
210
|
+
defined_links = response.get(links_field)
|
211
|
+
if defined_links is not None:
|
212
|
+
statistic.links.total += len(defined_links)
|
213
|
+
if is_selected:
|
214
|
+
collected_links.extend(defined_links.values())
|
215
|
+
finally:
|
216
|
+
self.resolver.pop_scope()
|
217
|
+
except SCHEMA_PARSING_ERRORS:
|
218
|
+
continue
|
219
|
+
|
220
|
+
def is_link_selected(link: dict) -> bool:
|
221
|
+
if "$ref" in link:
|
222
|
+
_, link = resolve(link["$ref"])
|
223
|
+
|
224
|
+
if "operationId" in link:
|
225
|
+
return link["operationId"] in selected_operations_by_id
|
226
|
+
else:
|
227
|
+
try:
|
228
|
+
scope, _ = resolve(link["operationRef"])
|
229
|
+
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
230
|
+
path = path.replace("~1", "/").replace("~0", "~")
|
231
|
+
return (method, path) in selected_operations_by_path
|
232
|
+
except Exception:
|
233
|
+
return False
|
234
|
+
|
235
|
+
for link in collected_links:
|
236
|
+
if is_link_selected(link):
|
237
|
+
statistic.links.selected += 1
|
238
|
+
|
239
|
+
return statistic
|
240
|
+
|
176
241
|
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
177
242
|
try:
|
178
243
|
paths = self.raw_schema["paths"]
|
@@ -193,48 +258,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
193
258
|
# Ignore errors
|
194
259
|
continue
|
195
260
|
|
196
|
-
@property
|
197
|
-
def operations_count(self) -> int:
|
198
|
-
total = 0
|
199
|
-
# Do not build a list from it
|
200
|
-
for _ in self._operation_iter():
|
201
|
-
total += 1
|
202
|
-
return total
|
203
|
-
|
204
|
-
@property
|
205
|
-
def links_count(self) -> int:
|
206
|
-
total = 0
|
207
|
-
resolve = self.resolver.resolve
|
208
|
-
links_field = self.links_field
|
209
|
-
for definition in self._operation_iter():
|
210
|
-
for response in definition.get("responses", {}).values():
|
211
|
-
if "$ref" in response:
|
212
|
-
_, response = resolve(response["$ref"])
|
213
|
-
defined_links = response.get(links_field)
|
214
|
-
if defined_links is not None:
|
215
|
-
total += len(defined_links)
|
216
|
-
return total
|
217
|
-
|
218
|
-
def override(
|
219
|
-
self,
|
220
|
-
*,
|
221
|
-
query: dict[str, str] | None = None,
|
222
|
-
headers: dict[str, str] | None = None,
|
223
|
-
cookies: dict[str, str] | None = None,
|
224
|
-
path_parameters: dict[str, str] | None = None,
|
225
|
-
) -> Callable[[GenericTest], GenericTest]:
|
226
|
-
"""Override Open API parameters with fixed values."""
|
227
|
-
|
228
|
-
def _add_override(test: GenericTest) -> GenericTest:
|
229
|
-
check_no_override_mark(test)
|
230
|
-
override = CaseOverride(
|
231
|
-
query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
|
232
|
-
)
|
233
|
-
set_override_mark(test, override)
|
234
|
-
return test
|
235
|
-
|
236
|
-
return _add_override
|
237
|
-
|
238
261
|
def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
|
239
262
|
while "$ref" in value:
|
240
263
|
_, value = self.resolver.resolve(value["$ref"])
|
@@ -253,9 +276,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
253
276
|
parameters = operation.get("parameters", ())
|
254
277
|
return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
|
255
278
|
|
256
|
-
def get_all_operations(
|
257
|
-
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
258
|
-
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
279
|
+
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
259
280
|
"""Iterate over all operations defined in the API.
|
260
281
|
|
261
282
|
Each yielded item is either `Ok` or `Err`, depending on the presence of errors during schema processing.
|
@@ -276,7 +297,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
276
297
|
paths = self.raw_schema["paths"]
|
277
298
|
except KeyError as exc:
|
278
299
|
# This field is optional in Open API 3.1
|
279
|
-
if version.parse(self.
|
300
|
+
if version.parse(self.specification.version) >= version.parse("3.1"):
|
280
301
|
return
|
281
302
|
# Missing `paths` is not recoverable
|
282
303
|
self._raise_invalid_schema(exc)
|
@@ -290,7 +311,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
290
311
|
should_skip = self._should_skip
|
291
312
|
collect_parameters = self.collect_parameters
|
292
313
|
make_operation = self.make_operation
|
293
|
-
hooks = self.hooks
|
294
314
|
for path, path_item in paths.items():
|
295
315
|
method = None
|
296
316
|
try:
|
@@ -314,50 +334,36 @@ class BaseOpenAPISchema(BaseSchema):
|
|
314
334
|
entry,
|
315
335
|
resolved,
|
316
336
|
scope,
|
317
|
-
with_security_parameters=generation_config.with_security_parameters
|
318
|
-
if generation_config
|
319
|
-
else None,
|
320
337
|
)
|
321
|
-
context = HookContext(operation=operation)
|
322
|
-
if (
|
323
|
-
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
324
|
-
or should_skip_operation(hooks, context)
|
325
|
-
or (hooks and should_skip_operation(hooks, context))
|
326
|
-
):
|
327
|
-
continue
|
328
338
|
yield Ok(operation)
|
329
339
|
except SCHEMA_PARSING_ERRORS as exc:
|
330
340
|
yield self._into_err(exc, path, method)
|
331
341
|
except SCHEMA_PARSING_ERRORS as exc:
|
332
342
|
yield self._into_err(exc, path, method)
|
333
343
|
|
334
|
-
def _into_err(self, error: Exception, path: str | None, method: str | None) -> Err[
|
344
|
+
def _into_err(self, error: Exception, path: str | None, method: str | None) -> Err[InvalidSchema]:
|
335
345
|
__tracebackhide__ = True
|
336
346
|
try:
|
337
|
-
|
338
|
-
|
339
|
-
except OperationSchemaError as exc:
|
347
|
+
self._raise_invalid_schema(error, path, method)
|
348
|
+
except InvalidSchema as exc:
|
340
349
|
return Err(exc)
|
341
350
|
|
342
351
|
def _raise_invalid_schema(
|
343
352
|
self,
|
344
353
|
error: Exception,
|
345
|
-
full_path: str | None = None,
|
346
354
|
path: str | None = None,
|
347
355
|
method: str | None = None,
|
348
356
|
) -> NoReturn:
|
349
357
|
__tracebackhide__ = True
|
350
|
-
if isinstance(error,
|
351
|
-
raise
|
352
|
-
error, path=path, method=method, full_path=full_path
|
353
|
-
) from None
|
358
|
+
if isinstance(error, RefResolutionError):
|
359
|
+
raise InvalidSchema.from_reference_resolution_error(error, path=path, method=method) from None
|
354
360
|
try:
|
355
361
|
self.validate()
|
356
362
|
except jsonschema.ValidationError as exc:
|
357
|
-
raise
|
358
|
-
exc, path=path, method=method,
|
363
|
+
raise InvalidSchema.from_jsonschema_error(
|
364
|
+
exc, path=path, method=method, config=self.config.output
|
359
365
|
) from None
|
360
|
-
raise
|
366
|
+
raise InvalidSchema(SCHEMA_ERROR_MESSAGE, path=path, method=method) from error
|
361
367
|
|
362
368
|
def validate(self) -> None:
|
363
369
|
with suppress(TypeError):
|
@@ -392,12 +398,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
392
398
|
raw: dict[str, Any],
|
393
399
|
resolved: dict[str, Any],
|
394
400
|
scope: str,
|
395
|
-
with_security_parameters: bool | None = None,
|
396
401
|
) -> APIOperation:
|
397
402
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
398
403
|
__tracebackhide__ = True
|
399
404
|
base_url = self.get_base_url()
|
400
|
-
operation: APIOperation[OpenAPIParameter
|
405
|
+
operation: APIOperation[OpenAPIParameter] = APIOperation(
|
401
406
|
path=path,
|
402
407
|
method=method,
|
403
408
|
definition=OperationDefinition(raw, resolved, scope),
|
@@ -407,12 +412,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
407
412
|
)
|
408
413
|
for parameter in parameters:
|
409
414
|
operation.add_parameter(parameter)
|
410
|
-
|
411
|
-
|
412
|
-
if with_security_parameters is not None
|
413
|
-
else self.generation_config.with_security_parameters
|
414
|
-
)
|
415
|
-
if with_security_parameters:
|
415
|
+
config = self.config.generation_for(operation=operation)
|
416
|
+
if config.with_security_parameters:
|
416
417
|
self.security.process_definitions(self.raw_schema, operation, self.resolver)
|
417
418
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
418
419
|
return operation
|
@@ -423,13 +424,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
423
424
|
self._resolver = InliningResolver(self.location or "", self.raw_schema)
|
424
425
|
return self._resolver
|
425
426
|
|
426
|
-
def get_content_types(self, operation: APIOperation, response:
|
427
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
427
428
|
"""Content types available for this API operation."""
|
428
429
|
raise NotImplementedError
|
429
430
|
|
430
|
-
def get_strategies_from_examples(
|
431
|
-
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
432
|
-
) -> list[SearchStrategy[Case]]:
|
431
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
433
432
|
"""Get examples from the API operation."""
|
434
433
|
raise NotImplementedError
|
435
434
|
|
@@ -521,22 +520,21 @@ class BaseOpenAPISchema(BaseSchema):
|
|
521
520
|
operation: APIOperation,
|
522
521
|
hooks: HookDispatcher | None = None,
|
523
522
|
auth_storage: AuthStorage | None = None,
|
524
|
-
|
525
|
-
generation_config: GenerationConfig | None = None,
|
523
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
526
524
|
**kwargs: Any,
|
527
525
|
) -> SearchStrategy:
|
528
|
-
return
|
526
|
+
return openapi_cases(
|
529
527
|
operation=operation,
|
530
|
-
auth_storage=auth_storage,
|
531
528
|
hooks=hooks,
|
532
|
-
|
533
|
-
|
529
|
+
auth_storage=auth_storage,
|
530
|
+
generation_mode=generation_mode,
|
534
531
|
**kwargs,
|
535
532
|
)
|
536
533
|
|
537
534
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
538
535
|
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
539
|
-
|
536
|
+
config = self.config.generation_for(operation=operation)
|
537
|
+
if config.with_security_parameters:
|
540
538
|
security_parameters = self.security.get_security_definitions_as_parameters(
|
541
539
|
self.raw_schema, operation, self.resolver, location
|
542
540
|
)
|
@@ -551,15 +549,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
551
549
|
raise NotImplementedError
|
552
550
|
|
553
551
|
def _get_response_definitions(
|
554
|
-
self, operation: APIOperation, response:
|
552
|
+
self, operation: APIOperation, response: Response
|
555
553
|
) -> tuple[list[str], dict[str, Any]] | None:
|
556
554
|
try:
|
557
555
|
responses = operation.definition.raw["responses"]
|
558
556
|
except KeyError as exc:
|
559
|
-
# Possible to get if `validate_schema=False` is passed during schema creation
|
560
557
|
path = operation.path
|
561
|
-
|
562
|
-
self._raise_invalid_schema(exc, full_path, path, operation.method)
|
558
|
+
self._raise_invalid_schema(exc, path, operation.method)
|
563
559
|
status_code = str(response.status_code)
|
564
560
|
if status_code in responses:
|
565
561
|
return self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
|
@@ -568,7 +564,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
568
564
|
return None
|
569
565
|
|
570
566
|
def get_headers(
|
571
|
-
self, operation: APIOperation, response:
|
567
|
+
self, operation: APIOperation, response: Response
|
572
568
|
) -> tuple[list[str], dict[str, dict[str, Any]] | None] | None:
|
573
569
|
resolved = self._get_response_definitions(operation, response)
|
574
570
|
if not resolved:
|
@@ -577,64 +573,17 @@ class BaseOpenAPISchema(BaseSchema):
|
|
577
573
|
return scopes, definitions.get("headers")
|
578
574
|
|
579
575
|
def as_state_machine(self) -> type[APIStateMachine]:
|
580
|
-
|
581
|
-
return create_state_machine(self)
|
582
|
-
except OperationNotFound as exc:
|
583
|
-
raise SchemaError(
|
584
|
-
type=SchemaErrorType.OPEN_API_INVALID_SCHEMA,
|
585
|
-
message=f"Invalid Open API link definition: Operation `{exc.item}` not found",
|
586
|
-
) from exc
|
587
|
-
|
588
|
-
def add_link(
|
589
|
-
self,
|
590
|
-
source: APIOperation,
|
591
|
-
target: str | APIOperation,
|
592
|
-
status_code: str | int,
|
593
|
-
parameters: dict[str, str] | None = None,
|
594
|
-
request_body: Any = None,
|
595
|
-
name: str | None = None,
|
596
|
-
) -> None:
|
597
|
-
"""Add a new Open API link to the schema definition.
|
598
|
-
|
599
|
-
:param APIOperation source: This operation is the source of data
|
600
|
-
:param target: This operation will receive the data from this link.
|
601
|
-
Can be an ``APIOperation`` instance or a reference like this - ``#/paths/~1users~1{userId}/get``
|
602
|
-
:param str status_code: The link is triggered when the source API operation responds with this status code.
|
603
|
-
:param parameters: A dictionary that describes how parameters should be extracted from the matched response.
|
604
|
-
The key represents the parameter name in the target API operation, and the value is a runtime
|
605
|
-
expression string.
|
606
|
-
:param request_body: A literal value or runtime expression to use as a request body when
|
607
|
-
calling the target operation.
|
608
|
-
:param str name: Explicit link name.
|
609
|
-
|
610
|
-
.. code-block:: python
|
611
|
-
|
612
|
-
schema = schemathesis.from_uri("http://0.0.0.0/schema.yaml")
|
613
|
-
|
614
|
-
schema.add_link(
|
615
|
-
source=schema["/users/"]["POST"],
|
616
|
-
target=schema["/users/{userId}"]["GET"],
|
617
|
-
status_code="201",
|
618
|
-
parameters={"userId": "$response.body#/id"},
|
619
|
-
)
|
620
|
-
"""
|
621
|
-
if parameters is None and request_body is None:
|
622
|
-
raise ValueError("You need to provide `parameters` or `request_body`.")
|
623
|
-
links.add_link(
|
624
|
-
resolver=self.resolver,
|
625
|
-
responses=self[source.path][source.method].definition.raw["responses"],
|
626
|
-
links_field=self.links_field,
|
627
|
-
parameters=parameters,
|
628
|
-
request_body=request_body,
|
629
|
-
status_code=status_code,
|
630
|
-
target=target,
|
631
|
-
name=name,
|
632
|
-
)
|
576
|
+
return create_state_machine(self)
|
633
577
|
|
634
578
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
635
579
|
result: dict[str, dict[str, Any]] = defaultdict(dict)
|
636
580
|
for status_code, link in links.get_all_links(operation):
|
637
|
-
|
581
|
+
if isinstance(link, Ok):
|
582
|
+
name = link.ok().name
|
583
|
+
else:
|
584
|
+
name = link.err().name
|
585
|
+
result[status_code][name] = link
|
586
|
+
|
638
587
|
return result
|
639
588
|
|
640
589
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
@@ -642,11 +591,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
642
591
|
|
643
592
|
@property
|
644
593
|
def validator_cls(self) -> type[jsonschema.Validator]:
|
645
|
-
if self.
|
594
|
+
if self.specification.version.startswith("3.1"):
|
646
595
|
return jsonschema.Draft202012Validator
|
647
596
|
return jsonschema.Draft4Validator
|
648
597
|
|
649
|
-
def validate_response(self, operation: APIOperation, response:
|
598
|
+
def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
|
599
|
+
__tracebackhide__ = True
|
650
600
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
651
601
|
status_code = str(response.status_code)
|
652
602
|
if status_code in responses:
|
@@ -660,32 +610,24 @@ class BaseOpenAPISchema(BaseSchema):
|
|
660
610
|
if not schema:
|
661
611
|
# No schema to check against
|
662
612
|
return None
|
663
|
-
|
664
|
-
|
665
|
-
if
|
666
|
-
|
667
|
-
formatted_content_types = [f"\n- `{content_type}`" for content_type in
|
613
|
+
content_types = response.headers.get("content-type")
|
614
|
+
failures: list[Failure] = []
|
615
|
+
if content_types is None:
|
616
|
+
all_media_types = self.get_content_types(operation, response)
|
617
|
+
formatted_content_types = [f"\n- `{content_type}`" for content_type in all_media_types]
|
668
618
|
message = f"The following media types are documented in the schema:{''.join(formatted_content_types)}"
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
errors.append(exc)
|
676
|
-
if content_type and not is_json_media_type(content_type):
|
677
|
-
_maybe_raise_one_or_more(errors)
|
619
|
+
failures.append(MissingContentType(operation=operation.label, message=message, media_types=all_media_types))
|
620
|
+
content_type = None
|
621
|
+
else:
|
622
|
+
content_type = content_types[0]
|
623
|
+
if content_type and not media_types.is_json(content_type):
|
624
|
+
_maybe_raise_one_or_more(failures)
|
678
625
|
return None
|
679
626
|
try:
|
680
|
-
data =
|
627
|
+
data = response.json()
|
681
628
|
except JSONDecodeError as exc:
|
682
|
-
|
683
|
-
|
684
|
-
try:
|
685
|
-
raise exc_class(context.title, context=context) from exc
|
686
|
-
except Exception as exc:
|
687
|
-
errors.append(exc)
|
688
|
-
_maybe_raise_one_or_more(errors)
|
629
|
+
failures.append(MalformedJson.from_exception(operation=operation.label, exc=exc))
|
630
|
+
_maybe_raise_one_or_more(failures)
|
689
631
|
with self._validating_response(scopes) as resolver:
|
690
632
|
try:
|
691
633
|
jsonschema.validate(
|
@@ -697,13 +639,14 @@ class BaseOpenAPISchema(BaseSchema):
|
|
697
639
|
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
698
640
|
)
|
699
641
|
except jsonschema.ValidationError as exc:
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
642
|
+
failures.append(
|
643
|
+
JsonSchemaError.from_exception(
|
644
|
+
operation=operation.label,
|
645
|
+
exc=exc,
|
646
|
+
config=operation.schema.config.output,
|
647
|
+
)
|
648
|
+
)
|
649
|
+
_maybe_raise_one_or_more(failures)
|
707
650
|
return None # explicitly return None for mypy
|
708
651
|
|
709
652
|
@contextmanager
|
@@ -734,7 +677,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
734
677
|
else:
|
735
678
|
break
|
736
679
|
else:
|
737
|
-
target.update(
|
680
|
+
target.update(transform(deepclone(schema), callback, self.nullable_name))
|
738
681
|
if self._inline_reference_cache:
|
739
682
|
components[INLINED_REFERENCES_KEY] = self._inline_reference_cache
|
740
683
|
self._rewritten_components = components
|
@@ -745,8 +688,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
745
688
|
|
746
689
|
Inlining components helps `hypothesis-jsonschema` generate data that involves non-resolved references.
|
747
690
|
"""
|
748
|
-
schema =
|
749
|
-
schema =
|
691
|
+
schema = deepclone(schema)
|
692
|
+
schema = transform(schema, self._rewrite_references, self.resolver)
|
750
693
|
# Only add definitions that are reachable from the schema via references
|
751
694
|
stack = [schema]
|
752
695
|
seen = set()
|
@@ -761,8 +704,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
761
704
|
pointer = reference[1:]
|
762
705
|
resolved = resolve_pointer(self.rewritten_components, pointer)
|
763
706
|
if resolved is UNRESOLVABLE:
|
764
|
-
raise
|
765
|
-
|
707
|
+
raise LoaderError(
|
708
|
+
LoaderErrorKind.OPEN_API_INVALID_SCHEMA,
|
766
709
|
message=f"Unresolvable JSON pointer in the schema: {pointer}",
|
767
710
|
)
|
768
711
|
if isinstance(resolved, dict):
|
@@ -796,7 +739,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
796
739
|
if key not in self._inline_reference_cache:
|
797
740
|
with resolver.resolving(reference) as resolved:
|
798
741
|
# Resolved object also may have references
|
799
|
-
self._inline_reference_cache[key] =
|
742
|
+
self._inline_reference_cache[key] = transform(
|
800
743
|
resolved, lambda s: self._rewrite_references(s, resolver)
|
801
744
|
)
|
802
745
|
# Rewrite the reference with the new location
|
@@ -804,12 +747,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
804
747
|
return schema
|
805
748
|
|
806
749
|
|
807
|
-
def _maybe_raise_one_or_more(
|
808
|
-
if not
|
750
|
+
def _maybe_raise_one_or_more(failures: list[Failure]) -> None:
|
751
|
+
if not failures:
|
809
752
|
return
|
810
|
-
if len(
|
811
|
-
raise
|
812
|
-
raise
|
753
|
+
if len(failures) == 1:
|
754
|
+
raise failures[0] from None
|
755
|
+
raise FailureGroup(failures) from None
|
813
756
|
|
814
757
|
|
815
758
|
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
@@ -898,17 +841,16 @@ class MethodMap(Mapping):
|
|
898
841
|
def __getitem__(self, item: str) -> APIOperation:
|
899
842
|
try:
|
900
843
|
return self._init_operation(item)
|
901
|
-
except
|
902
|
-
available_methods = ", ".join(
|
844
|
+
except LookupError as exc:
|
845
|
+
available_methods = ", ".join(key.upper() for key in self if key in HTTP_METHODS)
|
903
846
|
message = f"Method `{item.upper()}` not found."
|
904
847
|
if available_methods:
|
905
848
|
message += f" Available methods: {available_methods}"
|
906
|
-
raise
|
849
|
+
raise LookupError(message) from exc
|
907
850
|
|
908
851
|
|
909
852
|
OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
|
910
853
|
OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE = "multipart/form-data"
|
911
|
-
C = TypeVar("C", bound=Case)
|
912
854
|
|
913
855
|
|
914
856
|
class SwaggerV20(BaseOpenAPISchema):
|
@@ -921,12 +863,9 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
921
863
|
links_field = "x-links"
|
922
864
|
|
923
865
|
@property
|
924
|
-
def
|
925
|
-
|
926
|
-
|
927
|
-
@property
|
928
|
-
def verbose_name(self) -> str:
|
929
|
-
return f"Swagger {self.spec_version}"
|
866
|
+
def specification(self) -> Specification:
|
867
|
+
version = self.raw_schema.get("swagger", "2.0")
|
868
|
+
return Specification.openapi(version=version)
|
930
869
|
|
931
870
|
def _validate(self) -> None:
|
932
871
|
SWAGGER_20_VALIDATOR.validate(self.raw_schema)
|
@@ -962,6 +901,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
962
901
|
for media_type in body_media_types:
|
963
902
|
collected.append(OpenAPI20Body(definition=parameter, media_type=media_type))
|
964
903
|
else:
|
904
|
+
if parameter["in"] in ("header", "cookie"):
|
905
|
+
check_header(parameter)
|
965
906
|
collected.append(OpenAPI20Parameter(definition=parameter))
|
966
907
|
|
967
908
|
if form_parameters:
|
@@ -972,11 +913,9 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
972
913
|
)
|
973
914
|
return collected
|
974
915
|
|
975
|
-
def get_strategies_from_examples(
|
976
|
-
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
977
|
-
) -> list[SearchStrategy[Case]]:
|
916
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
978
917
|
"""Get examples from the API operation."""
|
979
|
-
return get_strategies_from_examples(operation,
|
918
|
+
return get_strategies_from_examples(operation, **kwargs)
|
980
919
|
|
981
920
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
982
921
|
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
@@ -989,7 +928,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
989
928
|
schema, self.nullable_name, is_response_schema=True, update_quantifiers=False
|
990
929
|
)
|
991
930
|
|
992
|
-
def get_content_types(self, operation: APIOperation, response:
|
931
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
993
932
|
produces = operation.definition.raw.get("produces", None)
|
994
933
|
if produces:
|
995
934
|
return produces
|
@@ -999,14 +938,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
999
938
|
return serialization.serialize_swagger2_parameters(definitions)
|
1000
939
|
|
1001
940
|
def prepare_multipart(
|
1002
|
-
self, form_data:
|
941
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
1003
942
|
) -> tuple[list | None, dict[str, Any] | None]:
|
1004
|
-
"""Prepare form data for sending with `requests`.
|
1005
|
-
|
1006
|
-
:param form_data: Raw generated data as a dictionary.
|
1007
|
-
:param operation: The tested API operation for which the data was generated.
|
1008
|
-
:return: `files` and `data` values for `requests.request`.
|
1009
|
-
"""
|
1010
943
|
files, data = [], {}
|
1011
944
|
# If there is no content types specified for the request or "application/x-www-form-urlencoded" is specified
|
1012
945
|
# explicitly, then use it., but if "multipart/form-data" is specified, then use it
|
@@ -1046,36 +979,33 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
1046
979
|
def make_case(
|
1047
980
|
self,
|
1048
981
|
*,
|
1049
|
-
case_cls: type[C],
|
1050
982
|
operation: APIOperation,
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
983
|
+
method: str | None = None,
|
984
|
+
path: str | None = None,
|
985
|
+
path_parameters: dict[str, Any] | None = None,
|
986
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
987
|
+
cookies: dict[str, Any] | None = None,
|
988
|
+
query: dict[str, Any] | None = None,
|
989
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
1056
990
|
media_type: str | None = None,
|
1057
|
-
|
1058
|
-
) ->
|
991
|
+
meta: CaseMetadata | None = None,
|
992
|
+
) -> Case:
|
1059
993
|
if body is not NOT_SET and media_type is None:
|
1060
994
|
media_type = operation._get_default_media_type()
|
1061
|
-
return
|
995
|
+
return Case(
|
1062
996
|
operation=operation,
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
997
|
+
method=method or operation.method.upper(),
|
998
|
+
path=path or operation.path,
|
999
|
+
path_parameters=path_parameters or {},
|
1000
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
1001
|
+
cookies=cookies or {},
|
1002
|
+
query=query or {},
|
1067
1003
|
body=body,
|
1068
1004
|
media_type=media_type,
|
1069
|
-
|
1005
|
+
meta=meta,
|
1070
1006
|
)
|
1071
1007
|
|
1072
1008
|
def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
|
1073
|
-
"""Get the `consumes` value for the given API operation.
|
1074
|
-
|
1075
|
-
:param definition: Raw API operation definition.
|
1076
|
-
:return: A list of media-types for this operation.
|
1077
|
-
:rtype: List[str]
|
1078
|
-
"""
|
1079
1009
|
global_consumes = self.raw_schema.get("consumes", [])
|
1080
1010
|
consumes = definition.get("consumes", [])
|
1081
1011
|
if not consumes:
|
@@ -1084,6 +1014,8 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
1084
1014
|
|
1085
1015
|
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
1086
1016
|
for parameter in definition.get("parameters", []):
|
1017
|
+
if "$ref" in parameter:
|
1018
|
+
_, parameter = self.resolver.resolve(parameter["$ref"])
|
1087
1019
|
if parameter["in"] == "body":
|
1088
1020
|
return parameter["schema"]
|
1089
1021
|
return None
|
@@ -1099,15 +1031,12 @@ class OpenApi30(SwaggerV20):
|
|
1099
1031
|
links_field = "links"
|
1100
1032
|
|
1101
1033
|
@property
|
1102
|
-
def
|
1103
|
-
|
1104
|
-
|
1105
|
-
@property
|
1106
|
-
def verbose_name(self) -> str:
|
1107
|
-
return f"Open API {self.spec_version}"
|
1034
|
+
def specification(self) -> Specification:
|
1035
|
+
version = self.raw_schema["openapi"]
|
1036
|
+
return Specification.openapi(version=version)
|
1108
1037
|
|
1109
1038
|
def _validate(self) -> None:
|
1110
|
-
if self.
|
1039
|
+
if self.specification.version.startswith("3.1"):
|
1111
1040
|
# Currently we treat Open API 3.1 as 3.0 in some regard
|
1112
1041
|
OPENAPI_31_VALIDATOR.validate(self.raw_schema)
|
1113
1042
|
else:
|
@@ -1126,7 +1055,12 @@ class OpenApi30(SwaggerV20):
|
|
1126
1055
|
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
1127
1056
|
) -> list[OpenAPIParameter]:
|
1128
1057
|
# Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
|
1129
|
-
collected: list[OpenAPIParameter] = [
|
1058
|
+
collected: list[OpenAPIParameter] = []
|
1059
|
+
|
1060
|
+
for parameter in parameters:
|
1061
|
+
if parameter["in"] in ("header", "cookie"):
|
1062
|
+
check_header(parameter)
|
1063
|
+
collected.append(OpenAPI30Parameter(definition=parameter))
|
1130
1064
|
if "requestBody" in definition:
|
1131
1065
|
required = definition["requestBody"].get("required", False)
|
1132
1066
|
description = definition["requestBody"].get("description")
|
@@ -1149,13 +1083,11 @@ class OpenApi30(SwaggerV20):
|
|
1149
1083
|
)
|
1150
1084
|
return scopes, None
|
1151
1085
|
|
1152
|
-
def get_strategies_from_examples(
|
1153
|
-
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
1154
|
-
) -> list[SearchStrategy[Case]]:
|
1086
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
1155
1087
|
"""Get examples from the API operation."""
|
1156
|
-
return get_strategies_from_examples(operation,
|
1088
|
+
return get_strategies_from_examples(operation, **kwargs)
|
1157
1089
|
|
1158
|
-
def get_content_types(self, operation: APIOperation, response:
|
1090
|
+
def get_content_types(self, operation: APIOperation, response: Response) -> list[str]:
|
1159
1091
|
resolved = self._get_response_definitions(operation, response)
|
1160
1092
|
if not resolved:
|
1161
1093
|
return []
|
@@ -1170,14 +1102,8 @@ class OpenApi30(SwaggerV20):
|
|
1170
1102
|
return list(request_body["content"])
|
1171
1103
|
|
1172
1104
|
def prepare_multipart(
|
1173
|
-
self, form_data:
|
1105
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
1174
1106
|
) -> tuple[list | None, dict[str, Any] | None]:
|
1175
|
-
"""Prepare form data for sending with `requests`.
|
1176
|
-
|
1177
|
-
:param form_data: Raw generated data as a dictionary.
|
1178
|
-
:param operation: The tested API operation for which the data was generated.
|
1179
|
-
:return: `files` and `data` values for `requests.request`.
|
1180
|
-
"""
|
1181
1107
|
files = []
|
1182
1108
|
definition = operation.definition.raw
|
1183
1109
|
if "$ref" in definition["requestBody"]:
|
@@ -1192,7 +1118,7 @@ class OpenApi30(SwaggerV20):
|
|
1192
1118
|
# Open API 3.0 requires media types to be present. We can get here only if the schema defines
|
1193
1119
|
# the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
|
1194
1120
|
for media_type, entry in content.items():
|
1195
|
-
main, sub =
|
1121
|
+
main, sub = media_types.parse(media_type)
|
1196
1122
|
if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
|
1197
1123
|
schema = entry.get("schema")
|
1198
1124
|
break
|
@@ -1221,8 +1147,8 @@ class OpenApi30(SwaggerV20):
|
|
1221
1147
|
else:
|
1222
1148
|
body = definition["requestBody"]
|
1223
1149
|
if "content" in body:
|
1224
|
-
main, sub =
|
1150
|
+
main, sub = media_types.parse(media_type)
|
1225
1151
|
for defined_media_type, item in body["content"].items():
|
1226
|
-
if
|
1152
|
+
if media_types.parse(defined_media_type) == (main, sub):
|
1227
1153
|
return item["schema"]
|
1228
1154
|
return None
|