schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl

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