schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

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