schemathesis 3.25.5__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 +793 -448
  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 +24 -4
  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 +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  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 +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  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 +60 -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 +79 -61
  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 +143 -31
  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 +368 -242
  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.5.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.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.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,56 +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 (
37
+ InternalError,
38
+ OperationNotFound,
36
39
  OperationSchemaError,
37
- UsageError,
40
+ SchemaError,
41
+ SchemaErrorType,
38
42
  get_missing_content_type_error,
39
43
  get_response_parsing_error,
40
44
  get_schema_validation_error,
41
- SchemaError,
42
- SchemaErrorType,
43
- OperationNotFound,
44
45
  )
46
+ from ...generation import DataGenerationMethod, GenerationConfig
45
47
  from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
46
48
  from ...internal.copy import fast_deepcopy
47
49
  from ...internal.jsonschema import traverse_schema
48
50
  from ...internal.result import Err, Ok, Result
49
51
  from ...models import APIOperation, Case, OperationDefinition
50
- from ...schemas import BaseSchema, APIOperationMap
52
+ from ...schemas import APIOperationMap, BaseSchema
51
53
  from ...stateful import Stateful, StatefulTest
52
- from ...stateful.state_machine import APIStateMachine
53
54
  from ...transports.content_types import is_json_media_type, parse_content_type
54
55
  from ...transports.responses import get_json
55
- from ...types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query, GenericTest
56
56
  from . import links, serialization
57
+ from ._cache import OperationCache
57
58
  from ._hypothesis import get_case_strategy
58
59
  from .converter import to_json_schema, to_json_schema_recursive
59
60
  from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
60
61
  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
62
  from .parameters import (
69
63
  OpenAPI20Body,
70
64
  OpenAPI20CompositeBody,
@@ -73,12 +67,23 @@ from .parameters import (
73
67
  OpenAPI30Parameter,
74
68
  OpenAPIParameter,
75
69
  )
76
- 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
+ )
77
77
  from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
78
78
  from .stateful import create_state_machine
79
79
 
80
80
  if TYPE_CHECKING:
81
+ from hypothesis.strategies import SearchStrategy
82
+
83
+ from ...auths import AuthStorage
84
+ from ...stateful.state_machine import APIStateMachine
81
85
  from ...transports.responses import GenericResponse
86
+ from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
82
87
 
83
88
  SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
84
89
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
@@ -90,12 +95,11 @@ 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
@@ -113,13 +117,24 @@ class BaseOpenAPISchema(BaseSchema):
113
117
  info = self.raw_schema["info"]
114
118
  return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
115
119
 
116
- def _store_operations(
117
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
118
- ) -> dict[str, APIOperationMap]:
119
- 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
120
135
 
121
136
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
122
- matches = get_close_matches(item, list(self.operations))
137
+ matches = get_close_matches(item, list(self))
123
138
  self._on_missing_operation(item, exc, matches)
124
139
 
125
140
  def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
@@ -128,14 +143,35 @@ class BaseOpenAPISchema(BaseSchema):
128
143
  message += f". Did you mean `{matches[0]}`?"
129
144
  raise OperationNotFound(message=message, item=item) from exc
130
145
 
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
- )
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)
139
175
 
140
176
  def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
141
177
  try:
@@ -143,18 +179,14 @@ class BaseOpenAPISchema(BaseSchema):
143
179
  except KeyError:
144
180
  return
145
181
  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
182
+ should_skip = self._should_skip
183
+ for path, path_item in paths.items():
150
184
  try:
151
- if "$ref" in methods:
152
- _, resolved_methods = resolve(methods["$ref"])
153
- else:
154
- resolved_methods = methods
185
+ if "$ref" in path_item:
186
+ _, path_item = resolve(path_item["$ref"])
155
187
  # 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):
188
+ for method, definition in path_item.items():
189
+ if should_skip(path, method, definition):
158
190
  continue
159
191
  yield definition
160
192
  except SCHEMA_PARSING_ERRORS:
@@ -172,11 +204,13 @@ class BaseOpenAPISchema(BaseSchema):
172
204
  @property
173
205
  def links_count(self) -> int:
174
206
  total = 0
207
+ resolve = self.resolver.resolve
208
+ links_field = self.links_field
175
209
  for definition in self._operation_iter():
176
210
  for response in definition.get("responses", {}).values():
177
211
  if "$ref" in response:
178
- _, response = self.resolver.resolve(response["$ref"])
179
- defined_links = response.get(self.links_field)
212
+ _, response = resolve(response["$ref"])
213
+ defined_links = response.get(links_field)
180
214
  if defined_links is not None:
181
215
  total += len(defined_links)
182
216
  return total
@@ -201,8 +235,26 @@ class BaseOpenAPISchema(BaseSchema):
201
235
 
202
236
  return _add_override
203
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
+
204
256
  def get_all_operations(
205
- self, hooks: HookDispatcher | None = None
257
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
206
258
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
207
259
  """Iterate over all operations defined in the API.
208
260
 
@@ -230,47 +282,52 @@ class BaseOpenAPISchema(BaseSchema):
230
282
  self._raise_invalid_schema(exc)
231
283
 
232
284
  context = HookContext()
233
- 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():
234
295
  method = None
235
296
  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):
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:
253
303
  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)
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)
274
331
  except SCHEMA_PARSING_ERRORS as exc:
275
332
  yield self._into_err(exc, path, method)
276
333
 
@@ -319,20 +376,23 @@ class BaseOpenAPISchema(BaseSchema):
319
376
  """
320
377
  raise NotImplementedError
321
378
 
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
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.
326
383
  if "$ref" in methods:
327
- return fast_deepcopy(self.resolver.resolve(methods["$ref"]))
328
- return self.resolver.resolution_scope, fast_deepcopy(methods)
384
+ return self.resolver.resolve(methods["$ref"])
385
+ return self.resolver.resolution_scope, methods
329
386
 
330
387
  def make_operation(
331
388
  self,
332
389
  path: str,
333
390
  method: str,
334
391
  parameters: list[OpenAPIParameter],
335
- raw_definition: OperationDefinition,
392
+ raw: dict[str, Any],
393
+ resolved: dict[str, Any],
394
+ scope: str,
395
+ with_security_parameters: bool | None = None,
336
396
  ) -> APIOperation:
337
397
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
338
398
  __tracebackhide__ = True
@@ -340,14 +400,20 @@ class BaseOpenAPISchema(BaseSchema):
340
400
  operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
341
401
  path=path,
342
402
  method=method,
343
- definition=raw_definition,
403
+ definition=OperationDefinition(raw, resolved, scope),
344
404
  base_url=base_url,
345
405
  app=self.app,
346
406
  schema=self,
347
407
  )
348
408
  for parameter in parameters:
349
409
  operation.add_parameter(parameter)
350
- 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)
351
417
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
352
418
  return operation
353
419
 
@@ -361,7 +427,9 @@ class BaseOpenAPISchema(BaseSchema):
361
427
  """Content types available for this API operation."""
362
428
  raise NotImplementedError
363
429
 
364
- 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]]:
365
433
  """Get examples from the API operation."""
366
434
  raise NotImplementedError
367
435
 
@@ -375,49 +443,78 @@ class BaseOpenAPISchema(BaseSchema):
375
443
 
376
444
  def get_operation_by_id(self, operation_id: str) -> APIOperation:
377
445
  """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())
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)
380
453
  try:
381
- return self._operations_by_id[operation_id]
454
+ entry = cache.get_definition_by_id(operation_id)
382
455
  except KeyError as exc:
383
- matches = get_close_matches(operation_id, list(self._operations_by_id))
456
+ matches = get_close_matches(operation_id, cache.known_operation_ids)
384
457
  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:
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:
392
482
  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)
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
+ )
403
492
 
404
493
  def get_operation_by_reference(self, reference: str) -> APIOperation:
405
494
  """Get local or external `APIOperation` instance by reference.
406
495
 
407
496
  Reference example: #/paths/~1users~1{user_id}/patch
408
497
  """
409
- 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)
410
503
  path, method = scope.rsplit("/", maxsplit=2)[-2:]
411
504
  path = path.replace("~1", "/").replace("~0", "~")
412
- 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)
413
512
  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)
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
421
518
 
422
519
  def get_case_strategy(
423
520
  self,
@@ -438,13 +535,14 @@ class BaseOpenAPISchema(BaseSchema):
438
535
  )
439
536
 
440
537
  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)
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)
448
546
  if definitions:
449
547
  return self._get_parameter_serializer(definitions)
450
548
  return None
@@ -452,9 +550,11 @@ class BaseOpenAPISchema(BaseSchema):
452
550
  def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
453
551
  raise NotImplementedError
454
552
 
455
- 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:
456
556
  try:
457
- responses = operation.definition.resolved["responses"]
557
+ responses = operation.definition.raw["responses"]
458
558
  except KeyError as exc:
459
559
  # Possible to get if `validate_schema=False` is passed during schema creation
460
560
  path = operation.path
@@ -462,16 +562,19 @@ class BaseOpenAPISchema(BaseSchema):
462
562
  self._raise_invalid_schema(exc, full_path, path, operation.method)
463
563
  status_code = str(response.status_code)
464
564
  if status_code in responses:
465
- return responses[status_code]
565
+ return self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
466
566
  if "default" in responses:
467
- return responses["default"]
567
+ return self.resolver.resolve_in_scope(responses["default"], operation.definition.scope)
468
568
  return None
469
569
 
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:
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:
473
575
  return None
474
- return definitions.get("headers")
576
+ scopes, definitions = resolved
577
+ return scopes, definitions.get("headers")
475
578
 
476
579
  def as_state_machine(self) -> type[APIStateMachine]:
477
580
  try:
@@ -517,46 +620,16 @@ class BaseOpenAPISchema(BaseSchema):
517
620
  """
518
621
  if parameters is None and request_body is None:
519
622
  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)
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
+ )
560
633
 
561
634
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
562
635
  result: dict[str, dict[str, Any]] = defaultdict(dict)
@@ -565,7 +638,13 @@ class BaseOpenAPISchema(BaseSchema):
565
638
  return result
566
639
 
567
640
  def get_tags(self, operation: APIOperation) -> list[str] | None:
568
- 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
569
648
 
570
649
  def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
571
650
  responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
@@ -588,7 +667,7 @@ class BaseOpenAPISchema(BaseSchema):
588
667
  formatted_content_types = [f"\n- `{content_type}`" for content_type in media_types]
589
668
  message = f"The following media types are documented in the schema:{''.join(formatted_content_types)}"
590
669
  try:
591
- raise get_missing_content_type_error()(
670
+ raise get_missing_content_type_error(operation.verbose_name)(
592
671
  failures.MissingContentType.title,
593
672
  context=failures.MissingContentType(message=message, media_types=media_types),
594
673
  )
@@ -600,26 +679,26 @@ class BaseOpenAPISchema(BaseSchema):
600
679
  try:
601
680
  data = get_json(response)
602
681
  except JSONDecodeError as exc:
603
- exc_class = get_response_parsing_error(exc)
682
+ exc_class = get_response_parsing_error(operation.verbose_name, exc)
604
683
  context = failures.JSONDecodeErrorContext.from_exception(exc)
605
684
  try:
606
685
  raise exc_class(context.title, context=context) from exc
607
686
  except Exception as exc:
608
687
  errors.append(exc)
609
688
  _maybe_raise_one_or_more(errors)
610
- resolver = ConvertingResolver(
611
- self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
612
- )
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
- with in_scopes(resolver, scopes):
689
+ with self._validating_response(scopes) as resolver:
618
690
  try:
619
- 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
+ )
620
699
  except jsonschema.ValidationError as exc:
621
- exc_class = get_schema_validation_error(exc)
622
- 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)
623
702
  try:
624
703
  raise exc_class(ctx.title, context=ctx) from exc
625
704
  except Exception as exc:
@@ -627,6 +706,14 @@ class BaseOpenAPISchema(BaseSchema):
627
706
  _maybe_raise_one_or_more(errors)
628
707
  return None # explicitly return None for mypy
629
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
+
630
717
  @property
631
718
  def rewritten_components(self) -> dict[str, Any]:
632
719
  if not hasattr(self, "_rewritten_components"):
@@ -720,10 +807,9 @@ class BaseOpenAPISchema(BaseSchema):
720
807
  def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
721
808
  if not errors:
722
809
  return
723
- elif len(errors) == 1:
810
+ if len(errors) == 1:
724
811
  raise errors[0]
725
- else:
726
- 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)
727
813
 
728
814
 
729
815
  def _make_reference_key(scopes: list[str], reference: str) -> str:
@@ -765,30 +851,58 @@ def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[
765
851
  yield
766
852
 
767
853
 
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):
854
+ @dataclass
855
+ class MethodMap(Mapping):
781
856
  """Container for accessing API operations.
782
857
 
783
858
  Provides a more specific error message if API operation is not found.
784
859
  """
785
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
+
786
898
  def __getitem__(self, item: str) -> APIOperation:
787
899
  try:
788
- return super().__getitem__(item)
900
+ return self._init_operation(item)
789
901
  except KeyError as exc:
790
902
  available_methods = ", ".join(map(str.upper, self))
791
- 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}"
792
906
  raise KeyError(message) from exc
793
907
 
794
908
 
@@ -808,7 +922,7 @@ class SwaggerV20(BaseOpenAPISchema):
808
922
 
809
923
  @property
810
924
  def spec_version(self) -> str:
811
- return self.raw_schema["swagger"]
925
+ return self.raw_schema.get("swagger", "2.0")
812
926
 
813
927
  @property
814
928
  def verbose_name(self) -> str:
@@ -858,18 +972,22 @@ class SwaggerV20(BaseOpenAPISchema):
858
972
  )
859
973
  return collected
860
974
 
861
- 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]]:
862
978
  """Get examples from the API operation."""
863
- return get_strategies_from_examples(operation, self.examples_field)
979
+ return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
864
980
 
865
981
  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)
982
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
867
983
  schema = definition.get("schema")
868
984
  if not schema:
869
985
  return scopes, None
870
986
  # Extra conversion to JSON Schema is needed here if there was one $ref in the input
871
987
  # because it is not converted
872
- 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
+ )
873
991
 
874
992
  def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
875
993
  produces = operation.definition.raw.get("produces", None)
@@ -902,7 +1020,7 @@ class SwaggerV20(BaseOpenAPISchema):
902
1020
  else:
903
1021
  files.append((name, file_value))
904
1022
 
905
- for parameter in operation.definition.parameters:
1023
+ for parameter in operation.body:
906
1024
  if isinstance(parameter, OpenAPI20CompositeBody):
907
1025
  for form_parameter in parameter.definition:
908
1026
  name = form_parameter.name
@@ -917,7 +1035,7 @@ class SwaggerV20(BaseOpenAPISchema):
917
1035
  return files or None, data or None
918
1036
 
919
1037
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
920
- return self._get_consumes_for_operation(operation.definition.resolved)
1038
+ return self._get_consumes_for_operation(operation.definition.raw)
921
1039
 
922
1040
  def make_case(
923
1041
  self,
@@ -930,20 +1048,10 @@ class SwaggerV20(BaseOpenAPISchema):
930
1048
  query: Query | None = None,
931
1049
  body: Body | NotSet = NOT_SET,
932
1050
  media_type: str | None = None,
1051
+ generation_time: float = 0.0,
933
1052
  ) -> C:
934
1053
  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
- )
1054
+ media_type = operation._get_default_media_type()
947
1055
  return case_cls(
948
1056
  operation=operation,
949
1057
  path_parameters=path_parameters,
@@ -952,6 +1060,7 @@ class SwaggerV20(BaseOpenAPISchema):
952
1060
  query=query,
953
1061
  body=body,
954
1062
  media_type=media_type,
1063
+ generation_time=generation_time,
955
1064
  )
956
1065
 
957
1066
  def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
@@ -1022,31 +1131,37 @@ class OpenApi30(SwaggerV20):
1022
1131
  return collected
1023
1132
 
1024
1133
  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)
1134
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
1026
1135
  options = iter(definition.get("content", {}).values())
1027
1136
  option = next(options, None)
1028
1137
  # "schema" is an optional key in the `MediaType` object
1029
1138
  if option and "schema" in option:
1030
1139
  # Extra conversion to JSON Schema is needed here if there was one $ref in the input
1031
1140
  # because it is not converted
1032
- 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
+ )
1033
1144
  return scopes, None
1034
1145
 
1035
- 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]]:
1036
1149
  """Get examples from the API operation."""
1037
- return get_strategies_from_examples(operation, self.examples_field)
1150
+ return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
1038
1151
 
1039
1152
  def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
1040
- definitions = self._get_response_definitions(operation, response)
1041
- if not definitions:
1153
+ resolved = self._get_response_definitions(operation, response)
1154
+ if not resolved:
1042
1155
  return []
1156
+ _, definitions = resolved
1043
1157
  return list(definitions.get("content", {}).keys())
1044
1158
 
1045
1159
  def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
1046
1160
  return serialization.serialize_openapi3_parameters(definitions)
1047
1161
 
1048
1162
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
1049
- 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"])
1050
1165
 
1051
1166
  def prepare_multipart(
1052
1167
  self, form_data: FormData, operation: APIOperation
@@ -1058,11 +1173,22 @@ class OpenApi30(SwaggerV20):
1058
1173
  :return: `files` and `data` values for `requests.request`.
1059
1174
  """
1060
1175
  files = []
1061
- 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"]
1062
1182
  # 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():
1183
+ # the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
1184
+ for media_type, entry in content.items():
1185
+ main, sub = parse_content_type(media_type)
1186
+ if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
1187
+ schema = entry.get("schema")
1188
+ break
1189
+ else:
1190
+ raise InternalError("No 'multipart/form-data' media type found in the schema")
1191
+ for name, property_schema in (schema or {}).get("properties", {}).items():
1066
1192
  if name in form_data:
1067
1193
  if isinstance(form_data[name], list):
1068
1194
  files.extend([(name, item) for item in form_data[name]])