schemathesis 3.13.0__py3-none-any.whl → 4.4.2__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 (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,729 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from itertools import chain
6
+ from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, Sequence, cast
7
+
8
+ from schemathesis.config import GenerationConfig
9
+ from schemathesis.core import NOT_SET, NotSet
10
+ from schemathesis.core.adapter import OperationParameter
11
+ from schemathesis.core.errors import InvalidSchema
12
+ from schemathesis.core.jsonschema import BundleError, Bundler
13
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
14
+ from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject
15
+ from schemathesis.core.parameters import HEADER_LOCATIONS, ParameterLocation
16
+ from schemathesis.core.validation import check_header_name
17
+ from schemathesis.generation.modes import GenerationMode
18
+ from schemathesis.schemas import APIOperation, ParameterSet
19
+ from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
20
+ from schemathesis.specs.openapi.adapter.references import maybe_resolve
21
+ from schemathesis.specs.openapi.converter import to_json_schema
22
+ from schemathesis.specs.openapi.formats import HEADER_FORMAT
23
+
24
+ if TYPE_CHECKING:
25
+ from hypothesis import strategies as st
26
+
27
+ from schemathesis.core.compat import RefResolver
28
+
29
+
30
+ MISSING_SCHEMA_OR_CONTENT_MESSAGE = (
31
+ "Can not generate data for {location} parameter `{name}`! "
32
+ "It should have either `schema` or `content` keywords defined"
33
+ )
34
+
35
+ INVALID_SCHEMA_MESSAGE = (
36
+ "Can not generate data for {location} parameter `{name}`! Its schema should be an object or boolean, got {schema}"
37
+ )
38
+
39
+ FORM_MEDIA_TYPES = frozenset(["multipart/form-data", "application/x-www-form-urlencoded"])
40
+
41
+
42
+ @dataclass
43
+ class OpenApiComponent(ABC):
44
+ definition: Mapping[str, Any]
45
+ is_required: bool
46
+ name_to_uri: dict[str, str]
47
+ adapter: SpecificationAdapter
48
+
49
+ __slots__ = (
50
+ "definition",
51
+ "is_required",
52
+ "name_to_uri",
53
+ "adapter",
54
+ "_optimized_schema",
55
+ "_unoptimized_schema",
56
+ "_raw_schema",
57
+ "_examples",
58
+ )
59
+
60
+ def __post_init__(self) -> None:
61
+ self._optimized_schema: JsonSchema | NotSet = NOT_SET
62
+ self._unoptimized_schema: JsonSchema | NotSet = NOT_SET
63
+ self._raw_schema: JsonSchema | NotSet = NOT_SET
64
+ self._examples: list | NotSet = NOT_SET
65
+
66
+ @property
67
+ def optimized_schema(self) -> JsonSchema:
68
+ """JSON schema optimized for data generation."""
69
+ if self._optimized_schema is NOT_SET:
70
+ self._optimized_schema = self._build_schema(optimize=True)
71
+ assert not isinstance(self._optimized_schema, NotSet)
72
+ return self._optimized_schema
73
+
74
+ @property
75
+ def unoptimized_schema(self) -> JsonSchema:
76
+ """JSON schema preserving original constraint structure."""
77
+ if self._unoptimized_schema is NOT_SET:
78
+ self._unoptimized_schema = self._build_schema(optimize=False)
79
+ assert not isinstance(self._unoptimized_schema, NotSet)
80
+ return self._unoptimized_schema
81
+
82
+ @property
83
+ def raw_schema(self) -> JsonSchema:
84
+ """Raw schema extracted from definition before JSON Schema conversion."""
85
+ if self._raw_schema is NOT_SET:
86
+ self._raw_schema = self._get_raw_schema()
87
+ assert not isinstance(self._raw_schema, NotSet)
88
+ return self._raw_schema
89
+
90
+ @abstractmethod
91
+ def _get_raw_schema(self) -> JsonSchema:
92
+ """Get the raw schema for this component."""
93
+ raise NotImplementedError
94
+
95
+ @abstractmethod
96
+ def _get_default_type(self) -> str | None:
97
+ """Get default type for this parameter."""
98
+ raise NotImplementedError
99
+
100
+ def _build_schema(self, *, optimize: bool) -> JsonSchema:
101
+ """Build JSON schema with optional optimizations for data generation."""
102
+ schema = to_json_schema(
103
+ self.raw_schema,
104
+ nullable_keyword=self.adapter.nullable_keyword,
105
+ update_quantifiers=optimize,
106
+ )
107
+
108
+ # Missing the `type` keyword may significantly slowdown data generation, ensure it is set
109
+ default_type = self._get_default_type()
110
+ if isinstance(schema, dict):
111
+ if default_type is not None:
112
+ schema.setdefault("type", default_type)
113
+ elif schema is True and default_type is not None:
114
+ # Restrict such cases too
115
+ schema = {"type": default_type}
116
+
117
+ return schema
118
+
119
+ @property
120
+ def examples(self) -> list:
121
+ """All examples extracted from definition.
122
+
123
+ Combines both single 'example' and 'examples' container values.
124
+ """
125
+ if self._examples is NOT_SET:
126
+ self._examples = self._extract_examples()
127
+ assert not isinstance(self._examples, NotSet)
128
+ return self._examples
129
+
130
+ def _extract_examples(self) -> list[object]:
131
+ """Extract examples from both single example and examples container."""
132
+ examples: list[object] = []
133
+
134
+ container = self.definition.get(self.adapter.examples_container_keyword)
135
+ if isinstance(container, dict):
136
+ examples.extend(ex["value"] for ex in container.values() if isinstance(ex, dict) and "value" in ex)
137
+ elif isinstance(container, list):
138
+ examples.extend(container)
139
+
140
+ example = self.definition.get(self.adapter.example_keyword, NOT_SET)
141
+ if example is not NOT_SET:
142
+ examples.append(example)
143
+
144
+ return examples
145
+
146
+
147
+ @dataclass
148
+ class OpenApiParameter(OpenApiComponent):
149
+ """OpenAPI operation parameter."""
150
+
151
+ @classmethod
152
+ def from_definition(
153
+ cls, *, definition: Mapping[str, Any], name_to_uri: dict[str, str], adapter: SpecificationAdapter
154
+ ) -> OpenApiParameter:
155
+ is_required = definition.get("required", False)
156
+ return cls(definition=definition, is_required=is_required, name_to_uri=name_to_uri, adapter=adapter)
157
+
158
+ @property
159
+ def name(self) -> str:
160
+ """Parameter name."""
161
+ return self.definition["name"]
162
+
163
+ @property
164
+ def location(self) -> ParameterLocation:
165
+ """Where this parameter is located."""
166
+ try:
167
+ return ParameterLocation(self.definition["in"])
168
+ except ValueError:
169
+ return ParameterLocation.UNKNOWN
170
+
171
+ def _get_raw_schema(self) -> JsonSchema:
172
+ """Get raw parameter schema."""
173
+ return self.adapter.extract_parameter_schema(self.definition)
174
+
175
+ def _get_default_type(self) -> str | None:
176
+ """Return default type if parameter is in string-type location."""
177
+ return "string" if self.location.is_in_header else None
178
+
179
+
180
+ @dataclass
181
+ class OpenApiBody(OpenApiComponent):
182
+ """OpenAPI request body."""
183
+
184
+ media_type: str
185
+ resource_name: str | None
186
+ name_to_uri: dict[str, str]
187
+
188
+ __slots__ = (
189
+ "definition",
190
+ "is_required",
191
+ "media_type",
192
+ "resource_name",
193
+ "name_to_uri",
194
+ "adapter",
195
+ "_optimized_schema",
196
+ "_unoptimized_schema",
197
+ "_raw_schema",
198
+ "_examples",
199
+ "_positive_strategy_cache",
200
+ "_negative_strategy_cache",
201
+ )
202
+
203
+ @classmethod
204
+ def from_definition(
205
+ cls,
206
+ *,
207
+ definition: Mapping[str, Any],
208
+ is_required: bool,
209
+ media_type: str,
210
+ resource_name: str | None,
211
+ name_to_uri: dict[str, str],
212
+ adapter: SpecificationAdapter,
213
+ ) -> OpenApiBody:
214
+ return cls(
215
+ definition=definition,
216
+ is_required=is_required,
217
+ media_type=media_type,
218
+ resource_name=resource_name,
219
+ name_to_uri=name_to_uri,
220
+ adapter=adapter,
221
+ )
222
+
223
+ @classmethod
224
+ def from_form_parameters(
225
+ cls,
226
+ *,
227
+ definition: Mapping[str, Any],
228
+ media_type: str,
229
+ name_to_uri: dict[str, str],
230
+ adapter: SpecificationAdapter,
231
+ ) -> OpenApiBody:
232
+ return cls(
233
+ definition=definition,
234
+ is_required=True,
235
+ media_type=media_type,
236
+ resource_name=None,
237
+ name_to_uri=name_to_uri,
238
+ adapter=adapter,
239
+ )
240
+
241
+ def __post_init__(self) -> None:
242
+ super().__post_init__()
243
+ self._positive_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
244
+ self._negative_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
245
+
246
+ @property
247
+ def location(self) -> ParameterLocation:
248
+ return ParameterLocation.BODY
249
+
250
+ @property
251
+ def name(self) -> str:
252
+ # The name doesn't matter but is here for the interface completeness.
253
+ return "body"
254
+
255
+ def _get_raw_schema(self) -> JsonSchema:
256
+ """Get raw body schema."""
257
+ return self.definition.get("schema", {})
258
+
259
+ def _get_default_type(self) -> str | None:
260
+ """Return default type if body is a form type."""
261
+ return "object" if self.media_type in FORM_MEDIA_TYPES else None
262
+
263
+ def get_property_content_type(self, property_name: str) -> str | None:
264
+ """Get custom contentType for a form property from `encoding` definition."""
265
+ encoding = self.definition.get("encoding", {})
266
+ property_encoding = encoding.get(property_name, {})
267
+ return property_encoding.get("contentType")
268
+
269
+ def get_strategy(
270
+ self,
271
+ operation: APIOperation,
272
+ generation_config: GenerationConfig,
273
+ generation_mode: GenerationMode,
274
+ ) -> st.SearchStrategy:
275
+ """Get a Hypothesis strategy for this body parameter."""
276
+ # Check cache based on generation mode
277
+ if generation_mode == GenerationMode.POSITIVE:
278
+ if self._positive_strategy_cache is not NOT_SET:
279
+ assert not isinstance(self._positive_strategy_cache, NotSet)
280
+ return self._positive_strategy_cache
281
+ elif self._negative_strategy_cache is not NOT_SET:
282
+ assert not isinstance(self._negative_strategy_cache, NotSet)
283
+ return self._negative_strategy_cache
284
+
285
+ # Import here to avoid circular dependency
286
+ from schemathesis.specs.openapi._hypothesis import GENERATOR_MODE_TO_STRATEGY_FACTORY
287
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
288
+
289
+ # Build the strategy
290
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
291
+ schema = self.optimized_schema
292
+ assert isinstance(operation.schema, BaseOpenAPISchema)
293
+ strategy = strategy_factory(
294
+ schema,
295
+ operation.label,
296
+ ParameterLocation.BODY,
297
+ self.media_type,
298
+ generation_config,
299
+ operation.schema.adapter.jsonschema_validator_cls,
300
+ )
301
+
302
+ # Cache the strategy
303
+ if generation_mode == GenerationMode.POSITIVE:
304
+ self._positive_strategy_cache = strategy
305
+ else:
306
+ self._negative_strategy_cache = strategy
307
+
308
+ return strategy
309
+
310
+
311
+ OPENAPI_20_EXCLUDE_KEYS = frozenset(["required", "name", "in", "title", "description"])
312
+
313
+
314
+ def extract_parameter_schema_v2(parameter: Mapping[str, Any]) -> JsonSchemaObject:
315
+ # In Open API 2.0, schema for non-body parameters lives directly in the parameter definition
316
+ return {key: value for key, value in parameter.items() if key not in OPENAPI_20_EXCLUDE_KEYS}
317
+
318
+
319
+ def extract_parameter_schema_v3(parameter: Mapping[str, Any]) -> JsonSchema:
320
+ if "schema" in parameter:
321
+ if not isinstance(parameter["schema"], (dict, bool)):
322
+ raise InvalidSchema(
323
+ INVALID_SCHEMA_MESSAGE.format(
324
+ location=parameter.get("in", ""),
325
+ name=parameter.get("name", "<UNKNOWN>"),
326
+ schema=parameter["schema"],
327
+ ),
328
+ )
329
+ return parameter["schema"]
330
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-10
331
+ # > The map MUST only contain one entry.
332
+ try:
333
+ content = parameter["content"]
334
+ except KeyError as exc:
335
+ raise InvalidSchema(
336
+ MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(
337
+ location=parameter.get("in", ""), name=parameter.get("name", "<UNKNOWN>")
338
+ ),
339
+ ) from exc
340
+ options = iter(content.values())
341
+ media_type_object = next(options)
342
+ return media_type_object.get("schema", {})
343
+
344
+
345
+ def _bundle_parameter(
346
+ parameter: Mapping, resolver: RefResolver, bundler: Bundler
347
+ ) -> tuple[dict[str, Any], dict[str, str]]:
348
+ """Bundle a parameter definition to make it self-contained."""
349
+ _, definition = maybe_resolve(parameter, resolver, "")
350
+ schema = definition.get("schema")
351
+ name_to_uri = {}
352
+ if schema is not None:
353
+ definition = {k: v for k, v in definition.items() if k != "schema"}
354
+ try:
355
+ bundled = bundler.bundle(schema, resolver, inline_recursive=True)
356
+ definition["schema"] = bundled.schema
357
+ name_to_uri = bundled.name_to_uri
358
+ except BundleError as exc:
359
+ location = parameter.get("in", "")
360
+ name = parameter.get("name", "<UNKNOWN>")
361
+ raise InvalidSchema.from_bundle_error(exc, location, name) from exc
362
+ return cast(dict, definition), name_to_uri
363
+
364
+
365
+ OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
366
+ OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE = "multipart/form-data"
367
+
368
+
369
+ def iter_parameters_v2(
370
+ definition: Mapping[str, Any],
371
+ shared_parameters: Sequence[Mapping[str, Any]],
372
+ default_media_types: list[str],
373
+ resolver: RefResolver,
374
+ adapter: SpecificationAdapter,
375
+ ) -> Iterator[OperationParameter]:
376
+ media_types = definition.get("consumes", default_media_types)
377
+ # For `in=body` parameters, we imply `application/json` as the default media type because it is the most common.
378
+ body_media_types = media_types or (OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE,)
379
+ # If an API operation has parameters with `in=formData`, Schemathesis should know how to serialize it.
380
+ # We can't be 100% sure what media type is expected by the server and chose `multipart/form-data` as
381
+ # the default because it is broader since it allows us to upload files.
382
+ form_data_media_types = media_types or (OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE,)
383
+
384
+ form_parameters = []
385
+ form_name_to_uri = {}
386
+ bundler = Bundler()
387
+ for parameter in chain(definition.get("parameters", []), shared_parameters):
388
+ parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
389
+ if parameter["in"] in HEADER_LOCATIONS:
390
+ check_header_name(parameter["name"])
391
+
392
+ if parameter["in"] == "formData":
393
+ # We need to gather form parameters first before creating a composite parameter for them
394
+ form_parameters.append(parameter)
395
+ form_name_to_uri.update(name_to_uri)
396
+ elif parameter["in"] == ParameterLocation.BODY:
397
+ # Take the original definition & extract the resource_name from there
398
+ resource_name = None
399
+ for param in chain(definition.get("parameters", []), shared_parameters):
400
+ _, param = maybe_resolve(param, resolver, "")
401
+ if param.get("in") == ParameterLocation.BODY:
402
+ if "$ref" in param["schema"]:
403
+ resource_name = resource_name_from_ref(param["schema"]["$ref"])
404
+ for media_type in body_media_types:
405
+ yield OpenApiBody.from_definition(
406
+ definition=parameter,
407
+ is_required=parameter.get("required", False),
408
+ media_type=media_type,
409
+ name_to_uri=name_to_uri,
410
+ resource_name=resource_name,
411
+ adapter=adapter,
412
+ )
413
+ else:
414
+ yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
415
+
416
+ if form_parameters:
417
+ form_data = form_data_to_json_schema(form_parameters)
418
+ for media_type in form_data_media_types:
419
+ # Individual `formData` parameters are joined into a single "composite" one.
420
+ yield OpenApiBody.from_form_parameters(
421
+ definition=form_data, media_type=media_type, name_to_uri=form_name_to_uri, adapter=adapter
422
+ )
423
+
424
+
425
+ def iter_parameters_v3(
426
+ definition: Mapping[str, Any],
427
+ shared_parameters: Sequence[Mapping[str, Any]],
428
+ default_media_types: list[str],
429
+ resolver: RefResolver,
430
+ adapter: SpecificationAdapter,
431
+ ) -> Iterator[OperationParameter]:
432
+ # Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
433
+ # TODO: Typing
434
+ operation = definition
435
+
436
+ bundler = Bundler()
437
+ for parameter in chain(definition.get("parameters", []), shared_parameters):
438
+ parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
439
+ if parameter["in"] in HEADER_LOCATIONS:
440
+ check_header_name(parameter["name"])
441
+
442
+ yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
443
+
444
+ request_body_or_ref = operation.get("requestBody")
445
+ if request_body_or_ref is not None:
446
+ scope, request_body_or_ref = maybe_resolve(request_body_or_ref, resolver, "")
447
+ # It could be an object inside `requestBodies`, which could be a reference itself
448
+ _, request_body = maybe_resolve(request_body_or_ref, resolver, scope)
449
+
450
+ required = request_body.get("required", False)
451
+ for media_type, content in request_body["content"].items():
452
+ resource_name = None
453
+ schema = content.get("schema")
454
+ name_to_uri = {}
455
+ if isinstance(schema, dict):
456
+ content = dict(content)
457
+ if "$ref" in schema:
458
+ resource_name = resource_name_from_ref(schema["$ref"])
459
+ try:
460
+ to_bundle = cast(dict[str, Any], schema)
461
+ bundled = bundler.bundle(to_bundle, resolver, inline_recursive=True)
462
+ content["schema"] = bundled.schema
463
+ name_to_uri = bundled.name_to_uri
464
+ except BundleError as exc:
465
+ raise InvalidSchema.from_bundle_error(exc, "body") from exc
466
+ yield OpenApiBody.from_definition(
467
+ definition=content,
468
+ is_required=required,
469
+ media_type=media_type,
470
+ resource_name=resource_name,
471
+ name_to_uri=name_to_uri,
472
+ adapter=adapter,
473
+ )
474
+
475
+
476
+ def resource_name_from_ref(reference: str) -> str:
477
+ return reference.rsplit("/", maxsplit=1)[1]
478
+
479
+
480
+ def build_path_parameter_v2(kwargs: Mapping[str, Any]) -> OpenApiParameter:
481
+ from schemathesis.specs.openapi.adapter import v2
482
+
483
+ return OpenApiParameter.from_definition(
484
+ definition={"in": ParameterLocation.PATH.value, "required": True, "type": "string", "minLength": 1, **kwargs},
485
+ name_to_uri={},
486
+ adapter=v2,
487
+ )
488
+
489
+
490
+ def build_path_parameter_v3_0(kwargs: Mapping[str, Any]) -> OpenApiParameter:
491
+ from schemathesis.specs.openapi.adapter import v3_0
492
+
493
+ return OpenApiParameter.from_definition(
494
+ definition={
495
+ "in": ParameterLocation.PATH.value,
496
+ "required": True,
497
+ "schema": {"type": "string", "minLength": 1},
498
+ **kwargs,
499
+ },
500
+ name_to_uri={},
501
+ adapter=v3_0,
502
+ )
503
+
504
+
505
+ def build_path_parameter_v3_1(kwargs: Mapping[str, Any]) -> OpenApiParameter:
506
+ from schemathesis.specs.openapi.adapter import v3_1
507
+
508
+ return OpenApiParameter.from_definition(
509
+ definition={
510
+ "in": ParameterLocation.PATH.value,
511
+ "required": True,
512
+ "schema": {"type": "string", "minLength": 1},
513
+ **kwargs,
514
+ },
515
+ name_to_uri={},
516
+ adapter=v3_1,
517
+ )
518
+
519
+
520
+ @dataclass
521
+ class OpenApiParameterSet(ParameterSet):
522
+ items: list[OpenApiParameter]
523
+ location: ParameterLocation
524
+
525
+ __slots__ = ("items", "location", "_schema", "_schema_cache", "_strategy_cache")
526
+
527
+ def __init__(self, location: ParameterLocation, items: list[OpenApiParameter] | None = None) -> None:
528
+ self.location = location
529
+ self.items = items or []
530
+ self._schema: dict | NotSet = NOT_SET
531
+ self._schema_cache: dict[frozenset[str], dict[str, Any]] = {}
532
+ self._strategy_cache: dict[tuple[frozenset[str], GenerationMode], st.SearchStrategy] = {}
533
+
534
+ @property
535
+ def schema(self) -> dict[str, Any]:
536
+ if self._schema is NOT_SET:
537
+ self._schema = parameters_to_json_schema(self.items, self.location)
538
+ assert not isinstance(self._schema, NotSet)
539
+ return self._schema
540
+
541
+ def get_schema_with_exclusions(self, exclude: Iterable[str]) -> dict[str, Any]:
542
+ """Get cached schema with specified parameters excluded."""
543
+ exclude_key = frozenset(exclude)
544
+
545
+ if exclude_key in self._schema_cache:
546
+ return self._schema_cache[exclude_key]
547
+
548
+ schema = self.schema
549
+ if exclude_key:
550
+ # Need to exclude some parameters - create a shallow copy to avoid mutating cached schema
551
+ schema = dict(schema)
552
+ if self.location == ParameterLocation.HEADER:
553
+ # Remove excluded headers case-insensitively
554
+ exclude_lower = {name.lower() for name in exclude_key}
555
+ schema["properties"] = {
556
+ key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
557
+ }
558
+ if "required" in schema:
559
+ schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
560
+ else:
561
+ # Non-header locations: remove by exact name
562
+ schema["properties"] = {
563
+ key: value for key, value in schema["properties"].items() if key not in exclude_key
564
+ }
565
+ if "required" in schema:
566
+ schema["required"] = [key for key in schema["required"] if key not in exclude_key]
567
+
568
+ self._schema_cache[exclude_key] = schema
569
+ return schema
570
+
571
+ def get_strategy(
572
+ self,
573
+ operation: APIOperation,
574
+ generation_config: GenerationConfig,
575
+ generation_mode: GenerationMode,
576
+ exclude: Iterable[str] = (),
577
+ ) -> st.SearchStrategy:
578
+ """Get a Hypothesis strategy for this parameter set with specified exclusions."""
579
+ exclude_key = frozenset(exclude)
580
+ cache_key = (exclude_key, generation_mode)
581
+
582
+ if cache_key in self._strategy_cache:
583
+ return self._strategy_cache[cache_key]
584
+
585
+ # Import here to avoid circular dependency
586
+ from hypothesis import strategies as st
587
+
588
+ from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query
589
+ from schemathesis.specs.openapi._hypothesis import (
590
+ GENERATOR_MODE_TO_STRATEGY_FACTORY,
591
+ _can_skip_header_filter,
592
+ jsonify_python_specific_types,
593
+ make_negative_strategy,
594
+ quote_all,
595
+ )
596
+ from schemathesis.specs.openapi.negative import GeneratedValue
597
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
598
+
599
+ # Get schema with exclusions
600
+ schema = self.get_schema_with_exclusions(exclude)
601
+
602
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
603
+
604
+ if not schema["properties"] and strategy_factory is make_negative_strategy:
605
+ # Nothing to negate - all properties were excluded
606
+ strategy = st.none()
607
+ else:
608
+ assert isinstance(operation.schema, BaseOpenAPISchema)
609
+ strategy = strategy_factory(
610
+ schema,
611
+ operation.label,
612
+ self.location,
613
+ None,
614
+ generation_config,
615
+ operation.schema.adapter.jsonschema_validator_cls,
616
+ )
617
+
618
+ # For negative strategies, we need to handle GeneratedValue wrappers
619
+ is_negative = strategy_factory is make_negative_strategy
620
+
621
+ serialize = operation.get_parameter_serializer(self.location)
622
+ if serialize is not None:
623
+ if is_negative:
624
+ # Apply serialize only to the value part of GeneratedValue
625
+ strategy = strategy.map(lambda x: GeneratedValue(serialize(x.value), x.meta))
626
+ else:
627
+ strategy = strategy.map(serialize)
628
+
629
+ filter_func = {
630
+ ParameterLocation.PATH: is_valid_path,
631
+ ParameterLocation.HEADER: is_valid_header,
632
+ ParameterLocation.COOKIE: is_valid_header,
633
+ ParameterLocation.QUERY: is_valid_query,
634
+ }[self.location]
635
+ # Headers with special format do not need filtration
636
+ if not (self.location.is_in_header and _can_skip_header_filter(schema)):
637
+ if is_negative:
638
+ # Apply filter only to the value part of GeneratedValue
639
+ strategy = strategy.filter(lambda x: filter_func(x.value))
640
+ else:
641
+ strategy = strategy.filter(filter_func)
642
+
643
+ # Path & query parameters will be cast to string anyway, but having their JSON equivalents for
644
+ # `True` / `False` / `None` improves chances of them passing validation in apps
645
+ # that expect boolean / null types
646
+ # and not aware of Python-specific representation of those types
647
+ if self.location == ParameterLocation.PATH:
648
+ if is_negative:
649
+ strategy = strategy.map(
650
+ lambda x: GeneratedValue(quote_all(jsonify_python_specific_types(x.value)), x.meta)
651
+ )
652
+ else:
653
+ strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
654
+ elif self.location == ParameterLocation.QUERY:
655
+ if is_negative:
656
+ strategy = strategy.map(lambda x: GeneratedValue(jsonify_python_specific_types(x.value), x.meta))
657
+ else:
658
+ strategy = strategy.map(jsonify_python_specific_types)
659
+
660
+ self._strategy_cache[cache_key] = strategy
661
+ return strategy
662
+
663
+
664
+ COMBINED_FORM_DATA_MARKER = "x-schemathesis-form-parameter"
665
+
666
+
667
+ def form_data_to_json_schema(parameters: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
668
+ """Convert raw form parameter definitions to a JSON Schema."""
669
+ parameter_data = (
670
+ (param["name"], extract_parameter_schema_v2(param), param.get("required", False)) for param in parameters
671
+ )
672
+
673
+ merged = _merge_parameters_to_object_schema(parameter_data, ParameterLocation.BODY)
674
+
675
+ return {"schema": merged, COMBINED_FORM_DATA_MARKER: True}
676
+
677
+
678
+ def parameters_to_json_schema(parameters: Iterable[OpenApiParameter], location: ParameterLocation) -> dict[str, Any]:
679
+ """Convert multiple Open API parameters to a JSON Schema."""
680
+ parameter_data = ((param.name, param.optimized_schema, param.is_required) for param in parameters)
681
+
682
+ return _merge_parameters_to_object_schema(parameter_data, location)
683
+
684
+
685
+ def _merge_parameters_to_object_schema(
686
+ parameters: Iterable[tuple[str, Any, bool]], location: ParameterLocation
687
+ ) -> dict[str, Any]:
688
+ """Merge parameter data into a JSON Schema object."""
689
+ properties = {}
690
+ required = []
691
+ bundled = {}
692
+
693
+ for name, subschema, is_required in parameters:
694
+ # Extract bundled data if present
695
+ if isinstance(subschema, dict) and BUNDLE_STORAGE_KEY in subschema:
696
+ subschema = dict(subschema)
697
+ subschema_bundle = subschema.pop(BUNDLE_STORAGE_KEY)
698
+ # NOTE: Bundled schema names are not overlapping as they were bundled via the same `Bundler` that
699
+ # ensures unique names
700
+ bundled.update(subschema_bundle)
701
+
702
+ # Apply location-specific adjustments to individual parameter schemas
703
+ if isinstance(subschema, dict):
704
+ # Headers: add HEADER_FORMAT for plain string types
705
+ if location.is_in_header and list(subschema) == ["type"] and subschema["type"] == "string":
706
+ subschema = {**subschema, "format": HEADER_FORMAT}
707
+
708
+ # Path parameters: ensure string types have minLength >= 1
709
+ elif location == ParameterLocation.PATH and subschema.get("type") == "string":
710
+ if "minLength" not in subschema:
711
+ subschema = {**subschema, "minLength": 1}
712
+
713
+ properties[name] = subschema
714
+
715
+ # Path parameters are always required
716
+ if (location == ParameterLocation.PATH or is_required) and name not in required:
717
+ required.append(name)
718
+
719
+ merged = {
720
+ "properties": properties,
721
+ "additionalProperties": False,
722
+ "type": "object",
723
+ }
724
+ if required:
725
+ merged["required"] = required
726
+ if bundled:
727
+ merged[BUNDLE_STORAGE_KEY] = bundled
728
+
729
+ return merged