schemathesis 3.15.4__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 (251) 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 -1219
  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 +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  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 +748 -82
  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 +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  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.15.4.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.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,80 +1,221 @@
1
- from typing import TYPE_CHECKING, Callable, Dict, List, Tuple
2
-
3
- import hypothesis.strategies as st
4
- from requests.structures import CaseInsensitiveDict
5
-
6
- from ....stateful import StepResult
7
- from ..links import OpenAPILink, get_all_links
8
- from ..utils import expand_status_code
9
-
10
- if TYPE_CHECKING:
11
- from ....models import APIOperation
12
-
13
- FilterFunction = Callable[[StepResult], bool]
14
-
15
-
16
- def apply(
17
- operation: "APIOperation",
18
- bundles: Dict[str, CaseInsensitiveDict],
19
- connections: Dict[str, List[st.SearchStrategy[Tuple[StepResult, OpenAPILink]]]],
20
- ) -> None:
21
- """Gather all connections based on Open API links definitions."""
22
- all_status_codes = list(operation.definition.resolved["responses"])
23
- for status_code, link in get_all_links(operation):
24
- target_operation = link.get_target_operation()
25
- strategy = bundles[operation.path][operation.method.upper()].filter(
26
- make_response_filter(status_code, all_status_codes)
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import lru_cache
5
+ from typing import Any, Callable
6
+
7
+ from schemathesis.core import NOT_SET, NotSet
8
+ from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError, format_transition
9
+ from schemathesis.core.parameters import ParameterLocation
10
+ from schemathesis.core.result import Err, Ok, Result
11
+ from schemathesis.generation.stateful.state_machine import ExtractedParam, StepOutput, Transition
12
+ from schemathesis.schemas import APIOperation
13
+ from schemathesis.specs.openapi import expressions
14
+
15
+ SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
16
+
17
+
18
+ @dataclass
19
+ class NormalizedParameter:
20
+ """Processed link parameter with resolved container information."""
21
+
22
+ location: ParameterLocation | None
23
+ name: str
24
+ expression: str
25
+ container_name: str
26
+ is_required: bool
27
+
28
+ __slots__ = ("location", "name", "expression", "container_name", "is_required")
29
+
30
+
31
+ @dataclass(repr=False)
32
+ class OpenApiLink:
33
+ """Represents an OpenAPI link between operations."""
34
+
35
+ name: str
36
+ status_code: str
37
+ source: APIOperation
38
+ target: APIOperation
39
+ parameters: list[NormalizedParameter]
40
+ body: dict[str, Any] | NotSet
41
+ merge_body: bool
42
+ is_inferred: bool
43
+
44
+ __slots__ = (
45
+ "name",
46
+ "status_code",
47
+ "source",
48
+ "target",
49
+ "parameters",
50
+ "body",
51
+ "merge_body",
52
+ "is_inferred",
53
+ "_cached_extract",
54
+ )
55
+
56
+ def __init__(self, name: str, status_code: str, definition: dict[str, Any], source: APIOperation):
57
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
58
+
59
+ self.name = name
60
+ self.status_code = status_code
61
+ self.source = source
62
+ assert isinstance(source.schema, BaseOpenAPISchema)
63
+ errors = []
64
+
65
+ get_operation: Callable[[str], APIOperation]
66
+ if "operationId" in definition:
67
+ operation_reference = definition["operationId"]
68
+ get_operation = source.schema.find_operation_by_id
69
+ else:
70
+ operation_reference = definition["operationRef"]
71
+ get_operation = source.schema.find_operation_by_reference
72
+
73
+ try:
74
+ self.target = get_operation(operation_reference)
75
+ target = self.target.label
76
+ except OperationNotFound:
77
+ target = operation_reference
78
+ errors.append(TransitionValidationError(f"Operation '{operation_reference}' not found"))
79
+
80
+ extension = definition.get(SCHEMATHESIS_LINK_EXTENSION)
81
+ self.parameters = self._normalize_parameters(definition.get("parameters", {}), errors)
82
+ self.body = definition.get("requestBody", NOT_SET)
83
+ self.merge_body = extension.get("merge_body", True) if extension else True
84
+ self.is_inferred = extension.get("is_inferred", False) if extension else False
85
+
86
+ if errors:
87
+ raise InvalidTransition(
88
+ name=self.name,
89
+ source=self.source.label,
90
+ target=target,
91
+ status_code=self.status_code,
92
+ errors=errors,
93
+ )
94
+
95
+ self._cached_extract = lru_cache(8)(self._extract_impl)
96
+
97
+ @property
98
+ def full_name(self) -> str:
99
+ return format_transition(self.source.label, self.status_code, self.name, self.target.label)
100
+
101
+ def _normalize_parameters(
102
+ self, parameters: dict[str, str], errors: list[TransitionValidationError]
103
+ ) -> list[NormalizedParameter]:
104
+ """Process link parameters and resolve their container locations.
105
+
106
+ Handles both explicit locations (e.g., "path.id") and implicit ones resolved from target operation.
107
+ """
108
+ result = []
109
+ for parameter, expression in parameters.items():
110
+ location: ParameterLocation | None
111
+ try:
112
+ # The parameter name is prefixed with its location. Example: `path.id`
113
+ _location, name = tuple(parameter.split("."))
114
+ location = ParameterLocation(_location)
115
+ except ValueError:
116
+ location = None
117
+ name = parameter
118
+
119
+ if isinstance(expression, str):
120
+ try:
121
+ parsed = expressions.parser.parse(expression)
122
+ # Find NonBodyRequest nodes that reference source parameters
123
+ for node in parsed:
124
+ if isinstance(node, expressions.nodes.NonBodyRequest):
125
+ # Check if parameter exists in source operation
126
+ if not any(
127
+ p.name == node.parameter and p.location == node.location
128
+ for p in self.source.iter_parameters()
129
+ ):
130
+ errors.append(
131
+ TransitionValidationError(
132
+ f"Expression `{expression}` references non-existent {node.location} parameter "
133
+ f"`{node.parameter}` in `{self.source.label}`"
134
+ )
135
+ )
136
+ except Exception as exc:
137
+ errors.append(TransitionValidationError(str(exc)))
138
+
139
+ is_required = False
140
+ if hasattr(self, "target"):
141
+ try:
142
+ container_name = self._get_parameter_container(location, name)
143
+ except TransitionValidationError as exc:
144
+ errors.append(exc)
145
+ continue
146
+
147
+ for param in self.target.iter_parameters():
148
+ if param.name == name:
149
+ is_required = param.is_required
150
+ break
151
+ else:
152
+ continue
153
+ result.append(NormalizedParameter(location, name, expression, container_name, is_required=is_required))
154
+ return result
155
+
156
+ def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
157
+ """Resolve parameter container either from explicit location or by looking up in target operation."""
158
+ if location:
159
+ return location.container_name
160
+
161
+ for param in self.target.iter_parameters():
162
+ if param.name == name:
163
+ return param.location.container_name
164
+ raise TransitionValidationError(f"Parameter `{name}` is not defined in API operation `{self.target.label}`")
165
+
166
+ def extract(self, output: StepOutput) -> Transition:
167
+ return self._cached_extract(StepOutputWrapper(output))
168
+
169
+ def _extract_impl(self, wrapper: StepOutputWrapper) -> Transition:
170
+ output = wrapper.output
171
+ return Transition(
172
+ id=self.full_name,
173
+ parent_id=output.case.id,
174
+ is_inferred=self.is_inferred,
175
+ parameters=self.extract_parameters(output),
176
+ request_body=self.extract_body(output),
27
177
  )
28
- connections[target_operation.verbose_name].append(_convert_strategy(strategy, link))
29
-
30
-
31
- def _convert_strategy(
32
- strategy: st.SearchStrategy[StepResult], link: OpenAPILink
33
- ) -> st.SearchStrategy[Tuple[StepResult, OpenAPILink]]:
34
- # This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
35
- return strategy.map(lambda out: (out, link))
36
-
37
-
38
- def make_response_filter(status_code: str, all_status_codes: List[str]) -> FilterFunction:
39
- """Create a filter for stored responses.
40
-
41
- This filter will decide whether some response is suitable to use as a source for requesting some API operation.
42
- """
43
- if status_code == "default":
44
- return default_status_code(all_status_codes)
45
- return match_status_code(status_code)
46
-
47
-
48
- def match_status_code(status_code: str) -> FilterFunction:
49
- """Create a filter function that matches all responses with the given status code.
50
-
51
- Note that the status code can contain "X", which means any digit.
52
- For example, 50X will match all status codes from 500 to 509.
53
- """
54
- status_codes = set(expand_status_code(status_code))
55
-
56
- def compare(result: StepResult) -> bool:
57
- return result.response.status_code in status_codes
58
-
59
- # This name is displayed in the resulting strategy representation. For example, if you run your tests with
60
- # `--hypothesis-show-statistics`, then you can see `Bundle(name='GET /users/{user_id}').filter(match_200_response)`
61
- # which gives you information about the particularly used filter.
62
- compare.__name__ = f"match_{status_code}_response"
63
-
64
- return compare
65
-
66
-
67
- def default_status_code(status_codes: List[str]) -> FilterFunction:
68
- """Create a filter that matches all "default" responses.
69
-
70
- In Open API, the "default" response is the one that is used if no other options were matched.
71
- Therefore we need to match only responses that were not matched by other listed status codes.
72
- """
73
- expanded_status_codes = {
74
- status_code for value in status_codes if value != "default" for status_code in expand_status_code(value)
75
- }
76
-
77
- def match_default_response(result: StepResult) -> bool:
78
- return result.response.status_code not in expanded_status_codes
79
178
 
80
- return match_default_response
179
+ def extract_parameters(self, output: StepOutput) -> dict[str, dict[str, ExtractedParam]]:
180
+ """Extract parameters using runtime expressions.
181
+
182
+ Returns a two-level dictionary: container -> parameter name -> extracted value
183
+ """
184
+ extracted: dict[str, dict[str, ExtractedParam]] = {}
185
+ for parameter in self.parameters:
186
+ container = extracted.setdefault(parameter.container_name, {})
187
+ value: Result[Any, Exception]
188
+ try:
189
+ value = Ok(expressions.evaluate(parameter.expression, output))
190
+ except Exception as exc:
191
+ value = Err(exc)
192
+ container[parameter.name] = ExtractedParam(
193
+ definition=parameter.expression, value=value, is_required=parameter.is_required
194
+ )
195
+ return extracted
196
+
197
+ def extract_body(self, output: StepOutput) -> ExtractedParam | None:
198
+ if not isinstance(self.body, NotSet):
199
+ value: Result[Any, Exception]
200
+ try:
201
+ value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
202
+ except Exception as exc:
203
+ value = Err(exc)
204
+ return ExtractedParam(definition=self.body, value=value, is_required=True)
205
+ return None
206
+
207
+
208
+ @dataclass
209
+ class StepOutputWrapper:
210
+ """Wrapper for StepOutput that uses only case_id for hash-based caching."""
211
+
212
+ output: StepOutput
213
+
214
+ __slots__ = ("output",)
215
+
216
+ def __hash__(self) -> int:
217
+ return hash(self.output.case.id)
218
+
219
+ def __eq__(self, other: object) -> bool:
220
+ assert isinstance(other, StepOutputWrapper)
221
+ return self.output.case.id == other.output.case.id
@@ -0,0 +1,3 @@
1
+ from schemathesis.specs.openapi.types import common, v2, v3
2
+
3
+ __all__ = ["common", "v2", "v3"]
@@ -0,0 +1,23 @@
1
+ """Common type definitions shared across OpenAPI versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Union
6
+
7
+ from typing_extensions import NotRequired, TypeAlias, TypedDict
8
+
9
+ Reference = TypedDict("Reference", {"$ref": str})
10
+ """JSON Reference object with $ref key."""
11
+
12
+ SchemaObject = TypedDict("SchemaObject", {"$ref": str})
13
+ """Schema object that may be a reference."""
14
+
15
+ _SecurityTypeKey = TypedDict("_SecurityTypeKey", {"x-original-security-type": NotRequired[str]})
16
+ """Type for x-original-security-type extension added by Schemathesis."""
17
+
18
+ # Type aliases for commonly used patterns
19
+ Schema: TypeAlias = Union[SchemaObject, bool]
20
+ """JSON Schema can be an object or boolean."""
21
+
22
+ SchemaOrRef: TypeAlias = Union[Mapping[str, Any], Reference]
23
+ """Schema definition that may be a reference or inline object."""
@@ -0,0 +1,129 @@
1
+ """Swagger 2.0 type definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Mapping, Union
6
+
7
+ from typing_extensions import NotRequired, TypeAlias, TypedDict
8
+
9
+ from schemathesis.specs.openapi.types.common import Reference, SchemaOrRef, _SecurityTypeKey
10
+
11
+
12
+ class BodyParameter(TypedDict):
13
+ """Swagger 2.0 body parameter."""
14
+
15
+ name: str
16
+ description: NotRequired[str]
17
+ required: NotRequired[bool]
18
+ schema: SchemaOrRef
19
+
20
+
21
+ _BodyParameterIn = TypedDict("_BodyParameterIn", {"in": Literal["body"]})
22
+
23
+
24
+ class BodyParameterWithIn(BodyParameter, _BodyParameterIn):
25
+ """Body parameter with 'in' field."""
26
+
27
+ pass
28
+
29
+
30
+ class NonBodyParameter(TypedDict):
31
+ """Swagger 2.0 non-body parameter (path/query/header/formData)."""
32
+
33
+ name: str
34
+ description: NotRequired[str]
35
+ required: NotRequired[bool]
36
+ type: NotRequired[Literal["string", "number", "integer", "boolean", "array", "file"]]
37
+ format: NotRequired[str]
38
+ items: NotRequired[SchemaOrRef]
39
+ collectionFormat: NotRequired[Literal["csv", "ssv", "tsv", "pipes"]]
40
+ default: NotRequired[Any]
41
+ maximum: NotRequired[float]
42
+ exclusiveMaximum: NotRequired[bool]
43
+ minimum: NotRequired[float]
44
+ exclusiveMinimum: NotRequired[bool]
45
+ maxLength: NotRequired[int]
46
+ minLength: NotRequired[int]
47
+ pattern: NotRequired[str]
48
+ maxItems: NotRequired[int]
49
+ minItems: NotRequired[int]
50
+ uniqueItems: NotRequired[bool]
51
+ enum: NotRequired[list[Any]]
52
+ multipleOf: NotRequired[float]
53
+
54
+
55
+ _NonBodyParameterIn = TypedDict("_NonBodyParameterIn", {"in": Literal["path", "query", "header", "formData"]})
56
+
57
+
58
+ class NonBodyParameterWithIn(NonBodyParameter, _NonBodyParameterIn):
59
+ """Non-body parameter with 'in' field."""
60
+
61
+ pass
62
+
63
+
64
+ Parameter: TypeAlias = Union[BodyParameterWithIn, NonBodyParameterWithIn, Reference]
65
+ """Swagger 2.0 parameter (body, non-body, or reference)."""
66
+
67
+
68
+ class Header(TypedDict):
69
+ """Swagger 2.0 response header."""
70
+
71
+ type: Literal["string", "number", "integer", "boolean", "array"]
72
+ description: NotRequired[str]
73
+ format: NotRequired[str]
74
+ items: NotRequired[SchemaOrRef]
75
+ collectionFormat: NotRequired[Literal["csv", "ssv", "tsv", "pipes"]]
76
+ default: NotRequired[Any]
77
+ maximum: NotRequired[float]
78
+ exclusiveMaximum: NotRequired[bool]
79
+ minimum: NotRequired[float]
80
+ exclusiveMinimum: NotRequired[bool]
81
+ maxLength: NotRequired[int]
82
+ minLength: NotRequired[int]
83
+ pattern: NotRequired[str]
84
+ maxItems: NotRequired[int]
85
+ minItems: NotRequired[int]
86
+ uniqueItems: NotRequired[bool]
87
+ enum: NotRequired[list[Any]]
88
+ multipleOf: NotRequired[float]
89
+
90
+
91
+ HeaderOrRef: TypeAlias = Union[Header, Reference]
92
+ """Header definition or reference."""
93
+
94
+ Headers: TypeAlias = Mapping[str, HeaderOrRef]
95
+ """Mapping from header name to header definition."""
96
+
97
+
98
+ class Response(TypedDict):
99
+ """Swagger 2.0 response object."""
100
+
101
+ description: str
102
+ schema: NotRequired[SchemaOrRef]
103
+ headers: NotRequired[dict[str, HeaderOrRef]]
104
+ examples: NotRequired[dict[str, Any]]
105
+
106
+
107
+ ResponseOrRef: TypeAlias = Union[Response, Reference]
108
+ """Response definition or reference."""
109
+
110
+ Responses: TypeAlias = Mapping[str, ResponseOrRef]
111
+ """Mapping from status code to response definition."""
112
+
113
+
114
+ class Operation(TypedDict):
115
+ responses: Responses
116
+ parameters: NotRequired[list[Parameter]]
117
+ consumes: NotRequired[list[str]]
118
+ produces: NotRequired[list[str]]
119
+
120
+
121
+ # Security parameter types
122
+ class SecurityParameter(NonBodyParameter, _SecurityTypeKey):
123
+ """Swagger 2.0 synthetic security parameter.
124
+
125
+ Created from security definitions (apiKey or basic auth).
126
+ Follows the same structure as NonBodyParameter since v2 has inline types.
127
+ """
128
+
129
+ pass
@@ -0,0 +1,134 @@
1
+ """OpenAPI 3.0 and 3.1 type definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Mapping, Union
6
+
7
+ from typing_extensions import NotRequired, TypeAlias, TypedDict
8
+
9
+ from schemathesis.specs.openapi.types.common import Reference, SchemaOrRef, _SecurityTypeKey
10
+
11
+
12
+ class Example(TypedDict):
13
+ value: NotRequired[Any]
14
+ externalValue: NotRequired[str]
15
+
16
+
17
+ class MediaType(TypedDict):
18
+ schema: SchemaOrRef
19
+ example: NotRequired[Any]
20
+
21
+
22
+ class Link(TypedDict):
23
+ operationId: NotRequired[str]
24
+ operationRef: NotRequired[str]
25
+ parameters: NotRequired[dict[str, Any]]
26
+ requestBody: NotRequired[Any]
27
+ server: NotRequired[Any]
28
+
29
+
30
+ class Header(TypedDict):
31
+ required: NotRequired[bool]
32
+
33
+
34
+ class Response(TypedDict):
35
+ headers: NotRequired[dict[str, HeaderOrRef]]
36
+ content: NotRequired[dict[str, MediaType]]
37
+ links: NotRequired[dict[str, LinkOrRef]]
38
+
39
+
40
+ class RequestBody(TypedDict):
41
+ content: dict[str, MediaType]
42
+ required: NotRequired[bool]
43
+
44
+
45
+ _ResponsesBase = Mapping[str, Union[Response, Reference]]
46
+
47
+
48
+ class Responses(_ResponsesBase):
49
+ pass
50
+
51
+
52
+ _HeadersBase = Mapping[str, Union[Header, Reference]]
53
+
54
+
55
+ class Headers(_HeadersBase):
56
+ pass
57
+
58
+
59
+ ExampleOrRef: TypeAlias = Union[Example, Reference]
60
+ """Example definition or reference."""
61
+
62
+ HeaderOrRef: TypeAlias = Union[Header, Reference]
63
+ """Header definition or reference."""
64
+
65
+ LinkOrRef: TypeAlias = Union[Link, Reference]
66
+ """Link definition or reference."""
67
+
68
+ ResponseOrRef: TypeAlias = Union[Response, Reference]
69
+ """Response definition or reference."""
70
+
71
+ RequestBodyOrRef: TypeAlias = Union[RequestBody, Reference]
72
+ """Request body definition or reference."""
73
+
74
+
75
+ class ParameterWithSchema(TypedDict):
76
+ """OpenAPI 3.0/3.1 parameter with schema."""
77
+
78
+ name: str
79
+ description: NotRequired[str]
80
+ required: NotRequired[bool]
81
+ deprecated: NotRequired[bool]
82
+ allowEmptyValue: NotRequired[bool]
83
+ schema: SchemaOrRef
84
+ style: NotRequired[str]
85
+ explode: NotRequired[bool]
86
+ allowReserved: NotRequired[bool]
87
+ example: NotRequired[Any]
88
+ examples: NotRequired[dict[str, ExampleOrRef]]
89
+
90
+
91
+ _ParameterIn = TypedDict("_ParameterIn", {"in": Literal["path", "query", "header", "cookie"]})
92
+
93
+
94
+ class ParameterWithSchemaAndIn(ParameterWithSchema, _ParameterIn):
95
+ """Parameter with schema and 'in' field."""
96
+
97
+ pass
98
+
99
+
100
+ class ParameterWithContent(TypedDict):
101
+ """OpenAPI 3.0/3.1 parameter with content."""
102
+
103
+ name: str
104
+ description: NotRequired[str]
105
+ required: NotRequired[bool]
106
+ deprecated: NotRequired[bool]
107
+ content: dict[str, MediaType]
108
+
109
+
110
+ class ParameterWithContentAndIn(ParameterWithContent, _ParameterIn):
111
+ """Parameter with content and 'in' field."""
112
+
113
+ pass
114
+
115
+
116
+ Parameter: TypeAlias = Union[ParameterWithSchemaAndIn, ParameterWithContentAndIn, Reference]
117
+ """OpenAPI 3.x parameter (with schema, with content, or reference)."""
118
+
119
+
120
+ class Operation(TypedDict):
121
+ responses: Responses
122
+ requestBody: NotRequired[RequestBodyOrRef]
123
+ parameters: NotRequired[list[Parameter]]
124
+
125
+
126
+ # Security parameter types
127
+ class SecurityParameter(ParameterWithSchema, _SecurityTypeKey):
128
+ """OpenAPI 3.x synthetic security parameter.
129
+
130
+ Created from security schemes (apiKey or http auth).
131
+ Follows the same structure as ParameterWithSchema since v3 uses nested schema.
132
+ """
133
+
134
+ pass
@@ -1,14 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import string
2
- from itertools import product
3
- from typing import Generator, Union
4
+ from itertools import chain, product
5
+ from typing import Generator
4
6
 
5
7
 
6
- def expand_status_code(status_code: Union[str, int]) -> Generator[int, None, None]:
8
+ def expand_status_code(status_code: str | int) -> Generator[int, None, None]:
7
9
  chars = [list(string.digits) if digit == "X" else [digit] for digit in str(status_code).upper()]
8
10
  for expanded in product(*chars):
9
11
  yield int("".join(expanded))
10
12
 
11
13
 
12
- def is_header_location(location: str) -> bool:
13
- """Whether this location affects HTTP headers."""
14
- return location in ("header", "cookie")
14
+ def expand_status_codes(status_codes: list[str]) -> set[int]:
15
+ return set(chain.from_iterable(expand_status_code(code) for code in status_codes))
@@ -0,0 +1,75 @@
1
+ """OpenAPI-specific static schema warnings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from schemathesis.config import SchemathesisWarning
9
+ from schemathesis.core import deserialization
10
+ from schemathesis.core.errors import MalformedMediaType
11
+ from schemathesis.core.jsonschema.types import get_type
12
+
13
+ if TYPE_CHECKING:
14
+ from schemathesis.schemas import APIOperation
15
+
16
+
17
+ @dataclass
18
+ class MissingDeserializerWarning:
19
+ """Warning for responses with structured schemas but no registered deserializer."""
20
+
21
+ operation_label: str
22
+ """Label of the operation (e.g., 'GET /users')."""
23
+
24
+ status_code: str
25
+ """HTTP status code for the response."""
26
+
27
+ content_type: str
28
+ """Media type that has no deserializer."""
29
+
30
+ __slots__ = ("operation_label", "status_code", "content_type")
31
+
32
+ @property
33
+ def kind(self) -> SchemathesisWarning:
34
+ """The warning kind for configuration matching."""
35
+ return SchemathesisWarning.MISSING_DESERIALIZER
36
+
37
+ @property
38
+ def message(self) -> str:
39
+ """Human-readable description of the warning."""
40
+ return f"Cannot validate response {self.status_code}: no deserializer registered for {self.content_type}"
41
+
42
+
43
+ def detect_missing_deserializers(operation: APIOperation) -> list[MissingDeserializerWarning]:
44
+ """Detect responses with structured schemas but no registered deserializer."""
45
+ warnings: list[MissingDeserializerWarning] = []
46
+
47
+ for status_code, response in operation.responses.items():
48
+ raw_schema = getattr(response, "get_raw_schema", lambda: None)()
49
+ if raw_schema is None:
50
+ continue
51
+
52
+ schema_types = get_type(raw_schema)
53
+ is_structured = any(t in ("object", "array") for t in schema_types)
54
+
55
+ if not is_structured:
56
+ continue
57
+
58
+ content_types = response.definition.get("content", {}).keys() if response.definition else []
59
+
60
+ for content_type in content_types:
61
+ try:
62
+ has_deserializer = deserialization.has_deserializer(content_type)
63
+ except MalformedMediaType:
64
+ continue
65
+
66
+ if not has_deserializer:
67
+ warnings.append(
68
+ MissingDeserializerWarning(
69
+ operation_label=operation.label,
70
+ status_code=status_code,
71
+ content_type=content_type,
72
+ )
73
+ )
74
+
75
+ return warnings