schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,7 @@ from difflib import get_close_matches
8
8
  from hashlib import sha1
9
9
  from json import JSONDecodeError
10
10
  from threading import RLock
11
+ from types import SimpleNamespace
11
12
  from typing import (
12
13
  TYPE_CHECKING,
13
14
  Any,
@@ -15,57 +16,49 @@ from typing import (
15
16
  ClassVar,
16
17
  Generator,
17
18
  Iterable,
19
+ Iterator,
20
+ Mapping,
18
21
  NoReturn,
19
22
  Sequence,
20
23
  TypeVar,
24
+ cast,
21
25
  )
22
26
  from urllib.parse import urlsplit
23
27
 
24
28
  import jsonschema
25
- from hypothesis.strategies import SearchStrategy
26
29
  from packaging import version
27
30
  from requests.structures import CaseInsensitiveDict
28
31
 
29
32
  from ... import experimental, failures
30
33
  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 ..._override import CaseOverride, check_no_override_mark, set_override_mark
34
35
  from ...constants import HTTP_METHODS, NOT_SET
35
36
  from ...exceptions import (
36
37
  InternalError,
38
+ OperationNotFound,
37
39
  OperationSchemaError,
38
- UsageError,
40
+ SchemaError,
41
+ SchemaErrorType,
39
42
  get_missing_content_type_error,
40
43
  get_response_parsing_error,
41
44
  get_schema_validation_error,
42
- SchemaError,
43
- SchemaErrorType,
44
- OperationNotFound,
45
45
  )
46
+ from ...generation import DataGenerationMethod, GenerationConfig
46
47
  from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
47
48
  from ...internal.copy import fast_deepcopy
48
49
  from ...internal.jsonschema import traverse_schema
49
50
  from ...internal.result import Err, Ok, Result
50
51
  from ...models import APIOperation, Case, OperationDefinition
51
- from ...schemas import BaseSchema, APIOperationMap
52
+ from ...schemas import APIOperationMap, BaseSchema
52
53
  from ...stateful import Stateful, StatefulTest
53
- from ...stateful.state_machine import APIStateMachine
54
54
  from ...transports.content_types import is_json_media_type, parse_content_type
55
55
  from ...transports.responses import get_json
56
- from ...types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query, GenericTest
57
56
  from . import links, serialization
57
+ from ._cache import OperationCache
58
58
  from ._hypothesis import get_case_strategy
59
59
  from .converter import to_json_schema, to_json_schema_recursive
60
60
  from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
61
61
  from .examples import get_strategies_from_examples
62
- from .filters import (
63
- should_skip_by_operation_id,
64
- should_skip_by_tag,
65
- should_skip_deprecated,
66
- should_skip_endpoint,
67
- should_skip_method,
68
- )
69
62
  from .parameters import (
70
63
  OpenAPI20Body,
71
64
  OpenAPI20CompositeBody,
@@ -74,12 +67,23 @@ from .parameters import (
74
67
  OpenAPI30Parameter,
75
68
  OpenAPIParameter,
76
69
  )
77
- from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver, resolve_pointer, UNRESOLVABLE
70
+ from .references import (
71
+ RECURSION_DEPTH_LIMIT,
72
+ UNRESOLVABLE,
73
+ ConvertingResolver,
74
+ InliningResolver,
75
+ resolve_pointer,
76
+ )
78
77
  from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
79
78
  from .stateful import create_state_machine
80
79
 
81
80
  if TYPE_CHECKING:
81
+ from hypothesis.strategies import SearchStrategy
82
+
83
+ from ...auths import AuthStorage
84
+ from ...stateful.state_machine import APIStateMachine
82
85
  from ...transports.responses import GenericResponse
86
+ from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
83
87
 
84
88
  SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
85
89
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
@@ -91,12 +95,11 @@ class BaseOpenAPISchema(BaseSchema):
91
95
  links_field: ClassVar[str] = ""
92
96
  header_required_field: ClassVar[str] = ""
93
97
  security: ClassVar[BaseSecurityProcessor] = None # type: ignore
94
- _operations_by_id: dict[str, APIOperation] = field(init=False)
98
+ _operation_cache: OperationCache = field(default_factory=OperationCache)
95
99
  _inline_reference_cache: dict[str, Any] = field(default_factory=dict)
96
100
  # Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
97
101
  # excessive resolving
98
102
  _inline_reference_cache_lock: RLock = field(default_factory=RLock)
99
- _override: CaseOverride | None = field(default=None)
100
103
  component_locations: ClassVar[tuple[tuple[str, ...], ...]] = ()
101
104
 
102
105
  @property
@@ -114,13 +117,24 @@ class BaseOpenAPISchema(BaseSchema):
114
117
  info = self.raw_schema["info"]
115
118
  return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
116
119
 
117
- def _store_operations(
118
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
119
- ) -> dict[str, APIOperationMap]:
120
- return operations_to_dict(operations)
120
+ def __iter__(self) -> Iterator[str]:
121
+ return iter(self.raw_schema.get("paths", {}))
122
+
123
+ def _get_operation_map(self, path: str) -> APIOperationMap:
124
+ cache = self._operation_cache
125
+ map = cache.get_map(path)
126
+ if map is not None:
127
+ return map
128
+ path_item = self.raw_schema.get("paths", {})[path]
129
+ scope, path_item = self._resolve_path_item(path_item)
130
+ self.dispatch_hook("before_process_path", HookContext(), path, path_item)
131
+ map = APIOperationMap(self, {})
132
+ map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
133
+ cache.insert_map(path, map)
134
+ return map
121
135
 
122
136
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
123
- matches = get_close_matches(item, list(self.operations))
137
+ matches = get_close_matches(item, list(self))
124
138
  self._on_missing_operation(item, exc, matches)
125
139
 
126
140
  def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
@@ -129,14 +143,35 @@ class BaseOpenAPISchema(BaseSchema):
129
143
  message += f". Did you mean `{matches[0]}`?"
130
144
  raise OperationNotFound(message=message, item=item) from exc
131
145
 
132
- def _should_skip(self, method: str, definition: dict[str, Any]) -> bool:
133
- return (
134
- method not in HTTP_METHODS
135
- or should_skip_method(method, self.method)
136
- or should_skip_deprecated(definition.get("deprecated", False), self.skip_deprecated_operations)
137
- or should_skip_by_tag(definition.get("tags"), self.tag)
138
- or should_skip_by_operation_id(definition.get("operationId"), self.operation_id)
139
- )
146
+ def _should_skip(
147
+ self,
148
+ path: str,
149
+ method: str,
150
+ definition: dict[str, Any],
151
+ _ctx_cache: SimpleNamespace = SimpleNamespace(
152
+ operation=APIOperation(
153
+ method="",
154
+ path="",
155
+ verbose_name="",
156
+ definition=OperationDefinition(raw=None, resolved=None, scope=""),
157
+ schema=None, # type: ignore
158
+ )
159
+ ),
160
+ ) -> bool:
161
+ if method not in HTTP_METHODS:
162
+ return True
163
+ if self.filter_set.is_empty():
164
+ return False
165
+ path = self.get_full_path(path)
166
+ # Attribute assignment is way faster than creating a new namespace every time
167
+ operation = _ctx_cache.operation
168
+ operation.method = method
169
+ operation.path = path
170
+ operation.verbose_name = f"{method.upper()} {path}"
171
+ operation.definition.raw = definition
172
+ operation.definition.resolved = definition
173
+ operation.schema = self
174
+ return not self.filter_set.match(_ctx_cache)
140
175
 
141
176
  def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
142
177
  try:
@@ -144,18 +179,14 @@ class BaseOpenAPISchema(BaseSchema):
144
179
  except KeyError:
145
180
  return
146
181
  resolve = self.resolver.resolve
147
- for path, methods in paths.items():
148
- full_path = self.get_full_path(path)
149
- if should_skip_endpoint(full_path, self.endpoint):
150
- continue
182
+ should_skip = self._should_skip
183
+ for path, path_item in paths.items():
151
184
  try:
152
- if "$ref" in methods:
153
- _, resolved_methods = resolve(methods["$ref"])
154
- else:
155
- resolved_methods = methods
185
+ if "$ref" in path_item:
186
+ _, path_item = resolve(path_item["$ref"])
156
187
  # Straightforward iteration is faster than converting to a set & calculating length.
157
- for method, definition in resolved_methods.items():
158
- if self._should_skip(method, definition):
188
+ for method, definition in path_item.items():
189
+ if should_skip(path, method, definition):
159
190
  continue
160
191
  yield definition
161
192
  except SCHEMA_PARSING_ERRORS:
@@ -173,11 +204,13 @@ class BaseOpenAPISchema(BaseSchema):
173
204
  @property
174
205
  def links_count(self) -> int:
175
206
  total = 0
207
+ resolve = self.resolver.resolve
208
+ links_field = self.links_field
176
209
  for definition in self._operation_iter():
177
210
  for response in definition.get("responses", {}).values():
178
211
  if "$ref" in response:
179
- _, response = self.resolver.resolve(response["$ref"])
180
- defined_links = response.get(self.links_field)
212
+ _, response = resolve(response["$ref"])
213
+ defined_links = response.get(links_field)
181
214
  if defined_links is not None:
182
215
  total += len(defined_links)
183
216
  return total
@@ -202,8 +235,26 @@ class BaseOpenAPISchema(BaseSchema):
202
235
 
203
236
  return _add_override
204
237
 
238
+ def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
239
+ while "$ref" in value:
240
+ _, value = self.resolver.resolve(value["$ref"])
241
+ return value
242
+
243
+ def _resolve_shared_parameters(self, path_item: Mapping[str, Any]) -> list[dict[str, Any]]:
244
+ return self.resolver.resolve_all(path_item.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
245
+
246
+ def _resolve_operation(self, operation: dict[str, Any]) -> dict[str, Any]:
247
+ return self.resolver.resolve_all(operation, RECURSION_DEPTH_LIMIT - 8)
248
+
249
+ def _collect_operation_parameters(
250
+ self, path_item: Mapping[str, Any], operation: dict[str, Any]
251
+ ) -> list[OpenAPIParameter]:
252
+ shared_parameters = self._resolve_shared_parameters(path_item)
253
+ parameters = operation.get("parameters", ())
254
+ return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
255
+
205
256
  def get_all_operations(
206
- self, hooks: HookDispatcher | None = None
257
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
207
258
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
208
259
  """Iterate over all operations defined in the API.
209
260
 
@@ -231,47 +282,52 @@ class BaseOpenAPISchema(BaseSchema):
231
282
  self._raise_invalid_schema(exc)
232
283
 
233
284
  context = HookContext()
234
- for path, methods in paths.items():
285
+ # Optimization: local variables are faster than attribute access
286
+ dispatch_hook = self.dispatch_hook
287
+ resolve_path_item = self._resolve_path_item
288
+ resolve_shared_parameters = self._resolve_shared_parameters
289
+ resolve_operation = self._resolve_operation
290
+ should_skip = self._should_skip
291
+ collect_parameters = self.collect_parameters
292
+ make_operation = self.make_operation
293
+ hooks = self.hooks
294
+ for path, path_item in paths.items():
235
295
  method = None
236
296
  try:
237
- full_path = self.get_full_path(path) # Should be available for later use
238
- if should_skip_endpoint(full_path, self.endpoint):
239
- continue
240
- self.dispatch_hook("before_process_path", context, path, methods)
241
- scope, raw_methods = self._resolve_methods(methods)
242
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
243
- for method, definition in raw_methods.items():
244
- try:
245
- # Setting a low recursion limit doesn't solve the problem with recursive references & inlining
246
- # too much but decreases the number of cases when Schemathesis stuck on this step.
247
- self.resolver.push_scope(scope)
248
- try:
249
- resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
250
- finally:
251
- self.resolver.pop_scope()
252
- # Only method definitions are parsed
253
- if self._should_skip(method, resolved_definition):
297
+ dispatch_hook("before_process_path", context, path, path_item)
298
+ scope, path_item = resolve_path_item(path_item)
299
+ with in_scope(self.resolver, scope):
300
+ shared_parameters = resolve_shared_parameters(path_item)
301
+ for method, entry in path_item.items():
302
+ if method not in HTTP_METHODS:
254
303
  continue
255
- parameters = self.collect_parameters(
256
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters),
257
- resolved_definition,
258
- )
259
- # To prevent recursion errors we need to pass not resolved schema as well
260
- # It could be used for response validation
261
- raw_definition = OperationDefinition(
262
- raw_methods[method], resolved_definition, scope, parameters
263
- )
264
- operation = self.make_operation(path, method, parameters, raw_definition)
265
- context = HookContext(operation=operation)
266
- if (
267
- should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
268
- or should_skip_operation(self.hooks, context)
269
- or (hooks and should_skip_operation(hooks, context))
270
- ):
271
- continue
272
- yield Ok(operation)
273
- except SCHEMA_PARSING_ERRORS as exc:
274
- yield self._into_err(exc, path, method)
304
+ try:
305
+ resolved = resolve_operation(entry)
306
+ if should_skip(path, method, resolved):
307
+ continue
308
+ parameters = resolved.get("parameters", ())
309
+ parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
310
+ operation = make_operation(
311
+ path,
312
+ method,
313
+ parameters,
314
+ entry,
315
+ resolved,
316
+ scope,
317
+ with_security_parameters=generation_config.with_security_parameters
318
+ if generation_config
319
+ else None,
320
+ )
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
+ yield Ok(operation)
329
+ except SCHEMA_PARSING_ERRORS as exc:
330
+ yield self._into_err(exc, path, method)
275
331
  except SCHEMA_PARSING_ERRORS as exc:
276
332
  yield self._into_err(exc, path, method)
277
333
 
@@ -320,20 +376,23 @@ class BaseOpenAPISchema(BaseSchema):
320
376
  """
321
377
  raise NotImplementedError
322
378
 
323
- def _resolve_methods(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
324
- # We need to know a proper scope in what methods are.
325
- # It will allow us to provide a proper reference resolving in `response_schema_conformance` and avoid
326
- # recursion errors
379
+ def _resolve_path_item(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
380
+ # The path item could be behind a reference
381
+ # In this case, we need to resolve it to get the proper scope for reference inside the item.
382
+ # It is mostly for validating responses.
327
383
  if "$ref" in methods:
328
- return fast_deepcopy(self.resolver.resolve(methods["$ref"]))
329
- return self.resolver.resolution_scope, fast_deepcopy(methods)
384
+ return self.resolver.resolve(methods["$ref"])
385
+ return self.resolver.resolution_scope, methods
330
386
 
331
387
  def make_operation(
332
388
  self,
333
389
  path: str,
334
390
  method: str,
335
391
  parameters: list[OpenAPIParameter],
336
- raw_definition: OperationDefinition,
392
+ raw: dict[str, Any],
393
+ resolved: dict[str, Any],
394
+ scope: str,
395
+ with_security_parameters: bool | None = None,
337
396
  ) -> APIOperation:
338
397
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
339
398
  __tracebackhide__ = True
@@ -341,14 +400,20 @@ class BaseOpenAPISchema(BaseSchema):
341
400
  operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
342
401
  path=path,
343
402
  method=method,
344
- definition=raw_definition,
403
+ definition=OperationDefinition(raw, resolved, scope),
345
404
  base_url=base_url,
346
405
  app=self.app,
347
406
  schema=self,
348
407
  )
349
408
  for parameter in parameters:
350
409
  operation.add_parameter(parameter)
351
- self.security.process_definitions(self.raw_schema, operation, self.resolver)
410
+ with_security_parameters = (
411
+ with_security_parameters
412
+ if with_security_parameters is not None
413
+ else self.generation_config.with_security_parameters
414
+ )
415
+ if with_security_parameters:
416
+ self.security.process_definitions(self.raw_schema, operation, self.resolver)
352
417
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
353
418
  return operation
354
419
 
@@ -362,7 +427,9 @@ class BaseOpenAPISchema(BaseSchema):
362
427
  """Content types available for this API operation."""
363
428
  raise NotImplementedError
364
429
 
365
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
430
+ def get_strategies_from_examples(
431
+ self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
432
+ ) -> list[SearchStrategy[Case]]:
366
433
  """Get examples from the API operation."""
367
434
  raise NotImplementedError
368
435
 
@@ -376,49 +443,78 @@ class BaseOpenAPISchema(BaseSchema):
376
443
 
377
444
  def get_operation_by_id(self, operation_id: str) -> APIOperation:
378
445
  """Get an `APIOperation` instance by its `operationId`."""
379
- if not hasattr(self, "_operations_by_id"):
380
- self._operations_by_id = dict(self._group_operations_by_id())
446
+ cache = self._operation_cache
447
+ cached = cache.get_operation_by_id(operation_id)
448
+ if cached is not None:
449
+ return cached
450
+ # Operation has not been accessed yet, need to populate the cache
451
+ if not cache.has_ids_to_definitions:
452
+ self._populate_operation_id_cache(cache)
381
453
  try:
382
- return self._operations_by_id[operation_id]
454
+ entry = cache.get_definition_by_id(operation_id)
383
455
  except KeyError as exc:
384
- matches = get_close_matches(operation_id, list(self._operations_by_id))
456
+ matches = get_close_matches(operation_id, cache.known_operation_ids)
385
457
  self._on_missing_operation(operation_id, exc, matches)
386
-
387
- def _group_operations_by_id(self) -> Generator[tuple[str, APIOperation], None, None]:
388
- for path, methods in self.raw_schema["paths"].items():
389
- scope, raw_methods = self._resolve_methods(methods)
390
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
391
- for method, definition in methods.items():
392
- if method not in HTTP_METHODS or "operationId" not in definition:
458
+ # It could've been already accessed in a different place
459
+ traversal_key = (entry.scope, entry.path, entry.method)
460
+ instance = cache.get_operation_by_traversal_key(traversal_key)
461
+ if instance is not None:
462
+ return instance
463
+ resolved = self._resolve_operation(entry.operation)
464
+ parameters = self._collect_operation_parameters(entry.path_item, resolved)
465
+ initialized = self.make_operation(entry.path, entry.method, parameters, entry.operation, resolved, entry.scope)
466
+ cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=operation_id)
467
+ return initialized
468
+
469
+ def _populate_operation_id_cache(self, cache: OperationCache) -> None:
470
+ """Collect all operation IDs from the schema."""
471
+ resolve = self.resolver.resolve
472
+ default_scope = self.resolver.resolution_scope
473
+ for path, path_item in self.raw_schema.get("paths", {}).items():
474
+ # If the path is behind a reference we have to keep the scope
475
+ # The scope is used to resolve nested components later on
476
+ if "$ref" in path_item:
477
+ scope, path_item = resolve(path_item["$ref"])
478
+ else:
479
+ scope = default_scope
480
+ for key, entry in path_item.items():
481
+ if key not in HTTP_METHODS:
393
482
  continue
394
- self.resolver.push_scope(scope)
395
- try:
396
- resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
397
- finally:
398
- self.resolver.pop_scope()
399
- parameters = self.collect_parameters(
400
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters), resolved_definition
401
- )
402
- raw_definition = OperationDefinition(raw_methods[method], resolved_definition, scope, parameters)
403
- yield resolved_definition["operationId"], self.make_operation(path, method, parameters, raw_definition)
483
+ if "operationId" in entry:
484
+ cache.insert_definition_by_id(
485
+ entry["operationId"],
486
+ path=path,
487
+ method=key,
488
+ scope=scope,
489
+ path_item=path_item,
490
+ operation=entry,
491
+ )
404
492
 
405
493
  def get_operation_by_reference(self, reference: str) -> APIOperation:
406
494
  """Get local or external `APIOperation` instance by reference.
407
495
 
408
496
  Reference example: #/paths/~1users~1{user_id}/patch
409
497
  """
410
- scope, data = self.resolver.resolve(reference)
498
+ cache = self._operation_cache
499
+ cached = cache.get_operation_by_reference(reference)
500
+ if cached is not None:
501
+ return cached
502
+ scope, operation = self.resolver.resolve(reference)
411
503
  path, method = scope.rsplit("/", maxsplit=2)[-2:]
412
504
  path = path.replace("~1", "/").replace("~0", "~")
413
- resolved_definition = self.resolver.resolve_all(data)
505
+ # Check the traversal cache as it could've been populated in other places
506
+ traversal_key = (self.resolver.resolution_scope, path, method)
507
+ cached = cache.get_operation_by_traversal_key(traversal_key)
508
+ if cached is not None:
509
+ return cached
510
+ with in_scope(self.resolver, scope):
511
+ resolved = self._resolve_operation(operation)
414
512
  parent_ref, _ = reference.rsplit("/", maxsplit=1)
415
- _, methods = self.resolver.resolve(parent_ref)
416
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
417
- parameters = self.collect_parameters(
418
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters), resolved_definition
419
- )
420
- raw_definition = OperationDefinition(data, resolved_definition, scope, parameters)
421
- return self.make_operation(path, method, parameters, raw_definition)
513
+ _, path_item = self.resolver.resolve(parent_ref)
514
+ parameters = self._collect_operation_parameters(path_item, resolved)
515
+ initialized = self.make_operation(path, method, parameters, operation, resolved, scope)
516
+ cache.insert_operation(initialized, traversal_key=traversal_key, reference=reference)
517
+ return initialized
422
518
 
423
519
  def get_case_strategy(
424
520
  self,
@@ -439,13 +535,14 @@ class BaseOpenAPISchema(BaseSchema):
439
535
  )
440
536
 
441
537
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
442
- definitions = [item for item in operation.definition.resolved.get("parameters", []) if item["in"] == location]
443
- security_parameters = self.security.get_security_definitions_as_parameters(
444
- self.raw_schema, operation, self.resolver, location
445
- )
446
- security_parameters = [item for item in security_parameters if item["in"] == location]
447
- if security_parameters:
448
- definitions.extend(security_parameters)
538
+ definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
539
+ if self.generation_config.with_security_parameters:
540
+ security_parameters = self.security.get_security_definitions_as_parameters(
541
+ self.raw_schema, operation, self.resolver, location
542
+ )
543
+ security_parameters = [item for item in security_parameters if item["in"] == location]
544
+ if security_parameters:
545
+ definitions.extend(security_parameters)
449
546
  if definitions:
450
547
  return self._get_parameter_serializer(definitions)
451
548
  return None
@@ -453,9 +550,11 @@ class BaseOpenAPISchema(BaseSchema):
453
550
  def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
454
551
  raise NotImplementedError
455
552
 
456
- def _get_response_definitions(self, operation: APIOperation, response: GenericResponse) -> dict[str, Any] | None:
553
+ def _get_response_definitions(
554
+ self, operation: APIOperation, response: GenericResponse
555
+ ) -> tuple[list[str], dict[str, Any]] | None:
457
556
  try:
458
- responses = operation.definition.resolved["responses"]
557
+ responses = operation.definition.raw["responses"]
459
558
  except KeyError as exc:
460
559
  # Possible to get if `validate_schema=False` is passed during schema creation
461
560
  path = operation.path
@@ -463,16 +562,19 @@ class BaseOpenAPISchema(BaseSchema):
463
562
  self._raise_invalid_schema(exc, full_path, path, operation.method)
464
563
  status_code = str(response.status_code)
465
564
  if status_code in responses:
466
- return responses[status_code]
565
+ return self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
467
566
  if "default" in responses:
468
- return responses["default"]
567
+ return self.resolver.resolve_in_scope(responses["default"], operation.definition.scope)
469
568
  return None
470
569
 
471
- def get_headers(self, operation: APIOperation, response: GenericResponse) -> dict[str, dict[str, Any]] | None:
472
- definitions = self._get_response_definitions(operation, response)
473
- if not definitions:
570
+ def get_headers(
571
+ self, operation: APIOperation, response: GenericResponse
572
+ ) -> tuple[list[str], dict[str, dict[str, Any]] | None] | None:
573
+ resolved = self._get_response_definitions(operation, response)
574
+ if not resolved:
474
575
  return None
475
- return definitions.get("headers")
576
+ scopes, definitions = resolved
577
+ return scopes, definitions.get("headers")
476
578
 
477
579
  def as_state_machine(self) -> type[APIStateMachine]:
478
580
  try:
@@ -518,46 +620,16 @@ class BaseOpenAPISchema(BaseSchema):
518
620
  """
519
621
  if parameters is None and request_body is None:
520
622
  raise ValueError("You need to provide `parameters` or `request_body`.")
521
- if hasattr(self, "_operations"):
522
- delattr(self, "_operations")
523
- for operation, methods in self.raw_schema["paths"].items():
524
- if operation == source.path:
525
- # Methods should be completely resolved now, otherwise they might miss a resolving scope when
526
- # they will be fully resolved later
527
- methods = self.resolver.resolve_all(methods)
528
- found = False
529
- for method, definition in methods.items():
530
- if method.upper() == source.method.upper():
531
- found = True
532
- links.add_link(
533
- responses=definition["responses"],
534
- links_field=self.links_field,
535
- parameters=parameters,
536
- request_body=request_body,
537
- status_code=status_code,
538
- target=target,
539
- name=name,
540
- )
541
- # If methods are behind a reference, then on the next resolving they will miss the new link
542
- # Therefore we need to modify it this way
543
- self.raw_schema["paths"][operation][method] = definition
544
- # The reference should be removed completely, otherwise new keys in this dictionary will be ignored
545
- # due to the `$ref` keyword behavior
546
- self.raw_schema["paths"][operation].pop("$ref", None)
547
- if found:
548
- return
549
- name = f"{source.method.upper()} {source.path}"
550
- # Use a name without basePath, as the user doesn't use it.
551
- # E.g. `source=schema["/users/"]["POST"]` without a prefix
552
- message = f"No such API operation: `{name}`."
553
- possibilities = [
554
- f"{op.ok().method.upper()} {op.ok().path}" for op in self.get_all_operations() if isinstance(op, Ok)
555
- ]
556
- matches = get_close_matches(name, possibilities)
557
- if matches:
558
- message += f" Did you mean `{matches[0]}`?"
559
- message += " Check if the requested API operation passes the filters in the schema."
560
- raise ValueError(message)
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
+ )
561
633
 
562
634
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
563
635
  result: dict[str, dict[str, Any]] = defaultdict(dict)
@@ -566,7 +638,13 @@ class BaseOpenAPISchema(BaseSchema):
566
638
  return result
567
639
 
568
640
  def get_tags(self, operation: APIOperation) -> list[str] | None:
569
- return operation.definition.resolved.get("tags")
641
+ return operation.definition.raw.get("tags")
642
+
643
+ @property
644
+ def validator_cls(self) -> type[jsonschema.Validator]:
645
+ if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
646
+ return jsonschema.Draft202012Validator
647
+ return jsonschema.Draft4Validator
570
648
 
571
649
  def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
572
650
  responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
@@ -589,7 +667,7 @@ class BaseOpenAPISchema(BaseSchema):
589
667
  formatted_content_types = [f"\n- `{content_type}`" for content_type in media_types]
590
668
  message = f"The following media types are documented in the schema:{''.join(formatted_content_types)}"
591
669
  try:
592
- raise get_missing_content_type_error()(
670
+ raise get_missing_content_type_error(operation.verbose_name)(
593
671
  failures.MissingContentType.title,
594
672
  context=failures.MissingContentType(message=message, media_types=media_types),
595
673
  )
@@ -601,26 +679,26 @@ class BaseOpenAPISchema(BaseSchema):
601
679
  try:
602
680
  data = get_json(response)
603
681
  except JSONDecodeError as exc:
604
- exc_class = get_response_parsing_error(exc)
682
+ exc_class = get_response_parsing_error(operation.verbose_name, exc)
605
683
  context = failures.JSONDecodeErrorContext.from_exception(exc)
606
684
  try:
607
685
  raise exc_class(context.title, context=context) from exc
608
686
  except Exception as exc:
609
687
  errors.append(exc)
610
688
  _maybe_raise_one_or_more(errors)
611
- resolver = ConvertingResolver(
612
- self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
613
- )
614
- if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
615
- cls = jsonschema.Draft202012Validator
616
- else:
617
- cls = jsonschema.Draft4Validator
618
- with in_scopes(resolver, scopes):
689
+ with self._validating_response(scopes) as resolver:
619
690
  try:
620
- jsonschema.validate(data, schema, cls=cls, resolver=resolver)
691
+ jsonschema.validate(
692
+ data,
693
+ schema,
694
+ cls=self.validator_cls,
695
+ resolver=resolver,
696
+ # Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
697
+ format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
698
+ )
621
699
  except jsonschema.ValidationError as exc:
622
- exc_class = get_schema_validation_error(exc)
623
- ctx = failures.ValidationErrorContext.from_exception(exc)
700
+ exc_class = get_schema_validation_error(operation.verbose_name, exc)
701
+ ctx = failures.ValidationErrorContext.from_exception(exc, output_config=operation.schema.output_config)
624
702
  try:
625
703
  raise exc_class(ctx.title, context=ctx) from exc
626
704
  except Exception as exc:
@@ -628,6 +706,14 @@ class BaseOpenAPISchema(BaseSchema):
628
706
  _maybe_raise_one_or_more(errors)
629
707
  return None # explicitly return None for mypy
630
708
 
709
+ @contextmanager
710
+ def _validating_response(self, scopes: list[str]) -> Generator[ConvertingResolver, None, None]:
711
+ resolver = ConvertingResolver(
712
+ self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
713
+ )
714
+ with in_scopes(resolver, scopes):
715
+ yield resolver
716
+
631
717
  @property
632
718
  def rewritten_components(self) -> dict[str, Any]:
633
719
  if not hasattr(self, "_rewritten_components"):
@@ -721,10 +807,9 @@ class BaseOpenAPISchema(BaseSchema):
721
807
  def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
722
808
  if not errors:
723
809
  return
724
- elif len(errors) == 1:
810
+ if len(errors) == 1:
725
811
  raise errors[0]
726
- else:
727
- raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
812
+ raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
728
813
 
729
814
 
730
815
  def _make_reference_key(scopes: list[str], reference: str) -> str:
@@ -766,30 +851,58 @@ def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[
766
851
  yield
767
852
 
768
853
 
769
- def operations_to_dict(
770
- operations: Generator[Result[APIOperation, OperationSchemaError], None, None],
771
- ) -> dict[str, APIOperationMap]:
772
- output: dict[str, APIOperationMap] = {}
773
- for result in operations:
774
- if isinstance(result, Ok):
775
- operation = result.ok()
776
- output.setdefault(operation.path, APIOperationMap(MethodMap()))
777
- output[operation.path][operation.method] = operation
778
- return output
779
-
780
-
781
- class MethodMap(CaseInsensitiveDict):
854
+ @dataclass
855
+ class MethodMap(Mapping):
782
856
  """Container for accessing API operations.
783
857
 
784
858
  Provides a more specific error message if API operation is not found.
785
859
  """
786
860
 
861
+ _parent: APIOperationMap
862
+ # Reference resolution scope
863
+ _scope: str
864
+ # Methods are stored for this path
865
+ _path: str
866
+ # Storage for definitions
867
+ _path_item: CaseInsensitiveDict
868
+
869
+ __slots__ = ("_parent", "_scope", "_path", "_path_item")
870
+
871
+ def __len__(self) -> int:
872
+ return len(self._path_item)
873
+
874
+ def __iter__(self) -> Iterator[str]:
875
+ return iter(self._path_item)
876
+
877
+ def _init_operation(self, method: str) -> APIOperation:
878
+ method = method.lower()
879
+ operation = self._path_item[method]
880
+ schema = cast(BaseOpenAPISchema, self._parent._schema)
881
+ cache = schema._operation_cache
882
+ path = self._path
883
+ scope = self._scope
884
+ traversal_key = (scope, path, method)
885
+ cached = cache.get_operation_by_traversal_key(traversal_key)
886
+ if cached is not None:
887
+ return cached
888
+ schema.resolver.push_scope(scope)
889
+ try:
890
+ resolved = schema._resolve_operation(operation)
891
+ finally:
892
+ schema.resolver.pop_scope()
893
+ parameters = schema._collect_operation_parameters(self._path_item, resolved)
894
+ initialized = schema.make_operation(path, method, parameters, operation, resolved, scope)
895
+ cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=resolved.get("operationId"))
896
+ return initialized
897
+
787
898
  def __getitem__(self, item: str) -> APIOperation:
788
899
  try:
789
- return super().__getitem__(item)
900
+ return self._init_operation(item)
790
901
  except KeyError as exc:
791
902
  available_methods = ", ".join(map(str.upper, self))
792
- message = f"Method `{item}` not found. Available methods: {available_methods}"
903
+ message = f"Method `{item.upper()}` not found."
904
+ if available_methods:
905
+ message += f" Available methods: {available_methods}"
793
906
  raise KeyError(message) from exc
794
907
 
795
908
 
@@ -859,18 +972,22 @@ class SwaggerV20(BaseOpenAPISchema):
859
972
  )
860
973
  return collected
861
974
 
862
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
975
+ def get_strategies_from_examples(
976
+ self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
977
+ ) -> list[SearchStrategy[Case]]:
863
978
  """Get examples from the API operation."""
864
- return get_strategies_from_examples(operation, self.examples_field)
979
+ return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
865
980
 
866
981
  def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
867
- scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
982
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
868
983
  schema = definition.get("schema")
869
984
  if not schema:
870
985
  return scopes, None
871
986
  # Extra conversion to JSON Schema is needed here if there was one $ref in the input
872
987
  # because it is not converted
873
- return scopes, to_json_schema_recursive(schema, self.nullable_name, is_response_schema=True)
988
+ return scopes, to_json_schema_recursive(
989
+ schema, self.nullable_name, is_response_schema=True, update_quantifiers=False
990
+ )
874
991
 
875
992
  def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
876
993
  produces = operation.definition.raw.get("produces", None)
@@ -903,7 +1020,7 @@ class SwaggerV20(BaseOpenAPISchema):
903
1020
  else:
904
1021
  files.append((name, file_value))
905
1022
 
906
- for parameter in operation.definition.parameters:
1023
+ for parameter in operation.body:
907
1024
  if isinstance(parameter, OpenAPI20CompositeBody):
908
1025
  for form_parameter in parameter.definition:
909
1026
  name = form_parameter.name
@@ -918,7 +1035,7 @@ class SwaggerV20(BaseOpenAPISchema):
918
1035
  return files or None, data or None
919
1036
 
920
1037
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
921
- return self._get_consumes_for_operation(operation.definition.resolved)
1038
+ return self._get_consumes_for_operation(operation.definition.raw)
922
1039
 
923
1040
  def make_case(
924
1041
  self,
@@ -931,20 +1048,10 @@ class SwaggerV20(BaseOpenAPISchema):
931
1048
  query: Query | None = None,
932
1049
  body: Body | NotSet = NOT_SET,
933
1050
  media_type: str | None = None,
1051
+ generation_time: float = 0.0,
934
1052
  ) -> C:
935
1053
  if body is not NOT_SET and media_type is None:
936
- # If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
937
- media_types = operation.get_request_payload_content_types()
938
- if len(media_types) == 1:
939
- # The only available option
940
- media_type = media_types[0]
941
- else:
942
- media_types_repr = ", ".join(media_types)
943
- raise UsageError(
944
- "Can not detect appropriate media type. "
945
- "You can either specify one of the defined media types "
946
- f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
947
- )
1054
+ media_type = operation._get_default_media_type()
948
1055
  return case_cls(
949
1056
  operation=operation,
950
1057
  path_parameters=path_parameters,
@@ -953,6 +1060,7 @@ class SwaggerV20(BaseOpenAPISchema):
953
1060
  query=query,
954
1061
  body=body,
955
1062
  media_type=media_type,
1063
+ generation_time=generation_time,
956
1064
  )
957
1065
 
958
1066
  def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
@@ -1023,31 +1131,37 @@ class OpenApi30(SwaggerV20):
1023
1131
  return collected
1024
1132
 
1025
1133
  def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
1026
- scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
1134
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
1027
1135
  options = iter(definition.get("content", {}).values())
1028
1136
  option = next(options, None)
1029
1137
  # "schema" is an optional key in the `MediaType` object
1030
1138
  if option and "schema" in option:
1031
1139
  # Extra conversion to JSON Schema is needed here if there was one $ref in the input
1032
1140
  # because it is not converted
1033
- return scopes, to_json_schema_recursive(option["schema"], self.nullable_name, is_response_schema=True)
1141
+ return scopes, to_json_schema_recursive(
1142
+ option["schema"], self.nullable_name, is_response_schema=True, update_quantifiers=False
1143
+ )
1034
1144
  return scopes, None
1035
1145
 
1036
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
1146
+ def get_strategies_from_examples(
1147
+ self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
1148
+ ) -> list[SearchStrategy[Case]]:
1037
1149
  """Get examples from the API operation."""
1038
- return get_strategies_from_examples(operation, self.examples_field)
1150
+ return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
1039
1151
 
1040
1152
  def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
1041
- definitions = self._get_response_definitions(operation, response)
1042
- if not definitions:
1153
+ resolved = self._get_response_definitions(operation, response)
1154
+ if not resolved:
1043
1155
  return []
1156
+ _, definitions = resolved
1044
1157
  return list(definitions.get("content", {}).keys())
1045
1158
 
1046
1159
  def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
1047
1160
  return serialization.serialize_openapi3_parameters(definitions)
1048
1161
 
1049
1162
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
1050
- return list(operation.definition.resolved["requestBody"]["content"].keys())
1163
+ request_body = self._resolve_until_no_references(operation.definition.raw["requestBody"])
1164
+ return list(request_body["content"])
1051
1165
 
1052
1166
  def prepare_multipart(
1053
1167
  self, form_data: FormData, operation: APIOperation
@@ -1059,17 +1173,22 @@ class OpenApi30(SwaggerV20):
1059
1173
  :return: `files` and `data` values for `requests.request`.
1060
1174
  """
1061
1175
  files = []
1062
- content = operation.definition.resolved["requestBody"]["content"]
1176
+ definition = operation.definition.raw
1177
+ if "$ref" in definition["requestBody"]:
1178
+ body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
1179
+ else:
1180
+ body = definition["requestBody"]
1181
+ content = body["content"]
1063
1182
  # Open API 3.0 requires media types to be present. We can get here only if the schema defines
1064
1183
  # the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
1065
1184
  for media_type, entry in content.items():
1066
1185
  main, sub = parse_content_type(media_type)
1067
- if main in ("*", "multipart") and sub in ("*", "form-data"):
1068
- schema = entry["schema"]
1186
+ if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
1187
+ schema = entry.get("schema")
1069
1188
  break
1070
1189
  else:
1071
1190
  raise InternalError("No 'multipart/form-data' media type found in the schema")
1072
- for name, property_schema in schema.get("properties", {}).items():
1191
+ for name, property_schema in (schema or {}).get("properties", {}).items():
1073
1192
  if name in form_data:
1074
1193
  if isinstance(form_data[name], list):
1075
1194
  files.extend([(name, item) for item in form_data[name]])