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
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Mapping, Protocol, Sequence, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from jsonschema.protocols import Validator
7
+
8
+ from schemathesis.core.adapter import OperationParameter
9
+ from schemathesis.core.compat import RefResolver
10
+ from schemathesis.core.jsonschema.types import JsonSchema
11
+
12
+ IterResponseExamples = Callable[[Mapping[str, Any], str], Iterator[tuple[str, object]]]
13
+ ExtractRawResponseSchema = Callable[[Mapping[str, Any]], Union["JsonSchema", None]]
14
+ ExtractResponseSchema = Callable[[Mapping[str, Any], "RefResolver", str, str], Union["JsonSchema", None]]
15
+ ExtractHeaderSchema = Callable[[Mapping[str, Any], "RefResolver", str, str], "JsonSchema"]
16
+ ExtractParameterSchema = Callable[[Mapping[str, Any]], "JsonSchema"]
17
+ ExtractSecurityParameters = Callable[
18
+ [Mapping[str, Any], Mapping[str, Any], "RefResolver"],
19
+ Iterator[Mapping[str, Any]],
20
+ ]
21
+ IterParameters = Callable[
22
+ [Mapping[str, Any], Sequence[Mapping[str, Any]], list[str], "RefResolver", "SpecificationAdapter"],
23
+ Iterable["OperationParameter"],
24
+ ]
25
+ BuildPathParameter = Callable[[Mapping[str, Any]], "OperationParameter"]
26
+
27
+
28
+ class SpecificationAdapter(Protocol):
29
+ """Protocol for abstracting over different API specification formats (OpenAPI 2/3, etc.)."""
30
+
31
+ # Keyword used to mark nullable fields (e.g., "x-nullable" in OpenAPI 2.0, "nullable" in 3.x)
32
+ nullable_keyword: str
33
+ # Keyword used for required / optional headers. Open API 2.0 does not expect `required` there
34
+ header_required_keyword: str
35
+ # Keyword for Open API links
36
+ links_keyword: str
37
+ # Keyword for a single example
38
+ example_keyword: str
39
+ # Keyword for examples container
40
+ examples_container_keyword: str
41
+
42
+ # Function to extract schema from parameter definition
43
+ extract_parameter_schema: ExtractParameterSchema
44
+ # Function to extract response schema from specification
45
+ extract_raw_response_schema: ExtractRawResponseSchema
46
+ extract_response_schema: ExtractResponseSchema
47
+ # Function to extract header schema from specification
48
+ extract_header_schema: ExtractHeaderSchema
49
+ # Function to iterate over API operation parameters
50
+ iter_parameters: IterParameters
51
+ # Function to create a new path parameter
52
+ build_path_parameter: BuildPathParameter
53
+ # Function to extract examples from response definition
54
+ iter_response_examples: IterResponseExamples
55
+ # Function to extract security parameters for an API operation
56
+ extract_security_parameters: ExtractSecurityParameters
57
+
58
+ # JSON Schema validator class appropriate for this specification version
59
+ jsonschema_validator_cls: type[Validator]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Mapping
4
+
5
+ if TYPE_CHECKING:
6
+ from schemathesis.core.compat import RefResolver
7
+
8
+
9
+ def maybe_resolve(item: Mapping[str, Any], resolver: RefResolver, scope: str) -> tuple[str, Mapping[str, Any]]:
10
+ reference = item.get("$ref")
11
+ if reference is not None:
12
+ # TODO: this one should be synchronized
13
+ resolver.push_scope(scope)
14
+ try:
15
+ return resolver.resolve(reference)
16
+ finally:
17
+ resolver.pop_scope()
18
+
19
+ return scope, item
@@ -0,0 +1,368 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, ItemsView, Iterator, Mapping, cast
5
+
6
+ from schemathesis.core import NOT_SET, NotSet
7
+ from schemathesis.core.compat import RefResolutionError, RefResolver
8
+ from schemathesis.core.errors import InvalidSchema
9
+ from schemathesis.core.jsonschema.bundler import bundle
10
+ from schemathesis.core.jsonschema.types import JsonSchema
11
+ from schemathesis.specs.openapi import types
12
+ from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
13
+ from schemathesis.specs.openapi.adapter.references import maybe_resolve
14
+ from schemathesis.specs.openapi.converter import to_json_schema
15
+ from schemathesis.specs.openapi.utils import expand_status_code
16
+
17
+ if TYPE_CHECKING:
18
+ from jsonschema.protocols import Validator
19
+
20
+
21
+ @dataclass
22
+ class OpenApiResponse:
23
+ """OpenAPI response definition."""
24
+
25
+ status_code: str
26
+ definition: Mapping[str, Any]
27
+ resolver: RefResolver
28
+ scope: str
29
+ adapter: SpecificationAdapter
30
+
31
+ __slots__ = ("status_code", "definition", "resolver", "scope", "adapter", "_schema", "_validator", "_headers")
32
+
33
+ def __post_init__(self) -> None:
34
+ self._schema: JsonSchema | None | NotSet = NOT_SET
35
+ self._validator: Validator | NotSet = NOT_SET
36
+ self._headers: OpenApiResponseHeaders | NotSet = NOT_SET
37
+
38
+ @property
39
+ def schema(self) -> JsonSchema | None:
40
+ """The response body schema extracted from its definition.
41
+
42
+ Returns `None` if the response has no schema.
43
+ """
44
+ if self._schema is NOT_SET:
45
+ self._schema = self.adapter.extract_response_schema(
46
+ self.definition, self.resolver, self.scope, self.adapter.nullable_keyword
47
+ )
48
+ assert not isinstance(self._schema, NotSet)
49
+ return self._schema
50
+
51
+ def get_raw_schema(self) -> JsonSchema | None:
52
+ """Raw and unresolved response schema."""
53
+ return self.adapter.extract_raw_response_schema(self.definition)
54
+
55
+ @property
56
+ def validator(self) -> Validator | None:
57
+ """JSON Schema validator for this response."""
58
+ from jsonschema import Draft202012Validator
59
+
60
+ schema = self.schema
61
+ if schema is None:
62
+ return None
63
+ if self._validator is NOT_SET:
64
+ self.adapter.jsonschema_validator_cls.check_schema(schema)
65
+ self._validator = self.adapter.jsonschema_validator_cls(
66
+ schema,
67
+ # Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
68
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
69
+ resolver=RefResolver.from_schema(schema),
70
+ )
71
+ assert not isinstance(self._validator, NotSet)
72
+ return self._validator
73
+
74
+ @property
75
+ def headers(self) -> OpenApiResponseHeaders:
76
+ """A collection of header definitions for this response."""
77
+ if self._headers is NOT_SET:
78
+ headers = self.definition.get("headers", {})
79
+ self._headers = OpenApiResponseHeaders(
80
+ dict(_iter_resolved_headers(headers, self.resolver, self.scope, self.adapter))
81
+ )
82
+ assert not isinstance(self._headers, NotSet)
83
+ return self._headers
84
+
85
+ def iter_examples(self) -> Iterator[tuple[str, object]]:
86
+ """Iterate over examples of this response."""
87
+ return self.adapter.iter_response_examples(self.definition, self.status_code)
88
+
89
+ def iter_links(self) -> Iterator[tuple[str, Mapping[str, Any]]]:
90
+ links = self.definition.get(self.adapter.links_keyword)
91
+ if links is None:
92
+ return
93
+ for name, link in links.items():
94
+ _, link = maybe_resolve(link, self.resolver, self.scope)
95
+ yield name, link
96
+
97
+
98
+ @dataclass
99
+ class OpenApiResponses:
100
+ """Collection of OpenAPI response definitions."""
101
+
102
+ _inner: dict[str, OpenApiResponse]
103
+ resolver: RefResolver
104
+ scope: str
105
+ adapter: SpecificationAdapter
106
+
107
+ __slots__ = ("_inner", "resolver", "scope", "adapter")
108
+
109
+ @classmethod
110
+ def from_definition(
111
+ cls,
112
+ definition: types.v3.Responses | types.v2.Responses,
113
+ resolver: RefResolver,
114
+ scope: str,
115
+ adapter: SpecificationAdapter,
116
+ ) -> OpenApiResponses:
117
+ """Build new collection of responses from their raw definition."""
118
+ return cls(
119
+ dict(_iter_resolved_responses(definition=definition, resolver=resolver, scope=scope, adapter=adapter)),
120
+ resolver=resolver,
121
+ scope=scope,
122
+ adapter=adapter,
123
+ )
124
+
125
+ def items(self) -> ItemsView[str, OpenApiResponse]:
126
+ return self._inner.items()
127
+
128
+ def get(self, key: str) -> OpenApiResponse | None:
129
+ return self._inner.get(key)
130
+
131
+ def add(self, status_code: str, definition: dict[str, Any]) -> OpenApiResponse:
132
+ instance = OpenApiResponse(
133
+ status_code=status_code,
134
+ definition=definition,
135
+ resolver=self.resolver,
136
+ scope=self.scope,
137
+ adapter=self.adapter,
138
+ )
139
+ self._inner[status_code] = instance
140
+ return instance
141
+
142
+ @property
143
+ def status_codes(self) -> tuple[str, ...]:
144
+ """All defined status codes."""
145
+ # Defined as a tuple, so it can be used in a cache key
146
+ return tuple(self._inner)
147
+
148
+ def find_by_status_code(self, status_code: int) -> OpenApiResponse | None:
149
+ """Find the most specific response definition matching the given HTTP status code."""
150
+ responses = self._inner
151
+ # Full match has the highest priority
152
+ full_match = responses.get(str(status_code))
153
+ if full_match is not None:
154
+ return full_match
155
+ # Then, ones with wildcards
156
+ keys = sorted(responses, key=lambda k: k.count("X"))
157
+ for key in keys:
158
+ if key == "default":
159
+ continue
160
+ status_codes = expand_status_code(key)
161
+ if status_code in status_codes:
162
+ return responses[key]
163
+ # The default response has the lowest priority
164
+ return responses.get("default")
165
+
166
+ def iter_successful_responses(self) -> Iterator[OpenApiResponse]:
167
+ """Iterate over all response definitions for successful responses."""
168
+ for response in self._inner.values():
169
+ if response.status_code.startswith("2"):
170
+ yield response
171
+
172
+ def iter_examples(self) -> Iterator[tuple[str, object]]:
173
+ """Iterate over all examples for all responses."""
174
+ for response in self.iter_successful_responses():
175
+ yield from response.iter_examples()
176
+
177
+
178
+ def _iter_resolved_responses(
179
+ definition: types.v3.Responses | types.v2.Responses,
180
+ resolver: RefResolver,
181
+ scope: str,
182
+ adapter: SpecificationAdapter,
183
+ ) -> Iterator[tuple[str, OpenApiResponse]]:
184
+ """Iterate and resolve response definitions."""
185
+ for key, response in definition.items():
186
+ status_code = str(key)
187
+ new_scope, resolved = maybe_resolve(response, resolver, scope)
188
+ # Resolve one more level to support slightly malformed schemas with nested $ref chains
189
+ # Some real-world schemas use: response -> $ref to definition -> $ref to actual schema
190
+ # While technically not spec-compliant, this pattern is common enough to warrant leniency
191
+ new_scope, resolved = maybe_resolve(resolved, resolver, new_scope)
192
+ yield (
193
+ status_code,
194
+ OpenApiResponse(
195
+ status_code=status_code, definition=resolved, resolver=resolver, scope=new_scope, adapter=adapter
196
+ ),
197
+ )
198
+
199
+
200
+ def extract_response_schema_v2(
201
+ response: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
202
+ ) -> JsonSchema | None:
203
+ """Extract and prepare response schema for Swagger 2.0."""
204
+ schema = extract_raw_response_schema_v2(response)
205
+ if schema is not None:
206
+ return _prepare_schema(schema, resolver, scope, nullable_keyword)
207
+ return None
208
+
209
+
210
+ def extract_raw_response_schema_v2(response: Mapping[str, Any]) -> JsonSchema | None:
211
+ """Extract raw schema from Swagger 2.0 response (schema is at top level)."""
212
+ return response.get("schema")
213
+
214
+
215
+ def extract_response_schema_v3(
216
+ response: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
217
+ ) -> JsonSchema | None:
218
+ schema = extract_raw_response_schema_v3(response)
219
+ if schema is not None:
220
+ return _prepare_schema(schema, resolver, scope, nullable_keyword)
221
+ return None
222
+
223
+
224
+ def extract_raw_response_schema_v3(response: Mapping[str, Any]) -> JsonSchema | None:
225
+ options = iter(response.get("content", {}).values())
226
+ media_type = next(options, None)
227
+ # "schema" is an optional key in the `MediaType` object
228
+ if media_type is not None:
229
+ return media_type.get("schema")
230
+ return None
231
+
232
+
233
+ def _prepare_schema(schema: JsonSchema, resolver: RefResolver, scope: str, nullable_keyword: str) -> JsonSchema:
234
+ schema = _bundle_in_scope(schema, resolver, scope)
235
+ # Do not clone the schema, as bundling already does it
236
+ return to_json_schema(schema, nullable_keyword, is_response_schema=True, update_quantifiers=False, clone=False)
237
+
238
+
239
+ def _bundle_in_scope(schema: JsonSchema, resolver: RefResolver, scope: str) -> JsonSchema:
240
+ resolver.push_scope(scope)
241
+ try:
242
+ return bundle(schema, resolver, inline_recursive=False).schema
243
+ except RefResolutionError as exc:
244
+ raise InvalidSchema.from_reference_resolution_error(exc, None, None) from None
245
+ finally:
246
+ resolver.pop_scope()
247
+
248
+
249
+ @dataclass
250
+ class OpenApiResponseHeaders:
251
+ """Collection of OpenAPI response header definitions."""
252
+
253
+ _inner: dict[str, OpenApiResponseHeader]
254
+
255
+ __slots__ = ("_inner",)
256
+
257
+ def __bool__(self) -> bool:
258
+ return bool(self._inner)
259
+
260
+ def items(self) -> ItemsView[str, OpenApiResponseHeader]:
261
+ return self._inner.items()
262
+
263
+
264
+ @dataclass
265
+ class OpenApiResponseHeader:
266
+ """OpenAPI response header definition."""
267
+
268
+ name: str
269
+ definition: Mapping[str, Any]
270
+ resolver: RefResolver
271
+ scope: str
272
+ adapter: SpecificationAdapter
273
+
274
+ __slots__ = ("name", "definition", "resolver", "scope", "adapter", "_schema", "_validator")
275
+
276
+ def __post_init__(self) -> None:
277
+ self._schema: JsonSchema | NotSet = NOT_SET
278
+ self._validator: Validator | NotSet = NOT_SET
279
+
280
+ @property
281
+ def is_required(self) -> bool:
282
+ return self.definition.get(self.adapter.header_required_keyword, False)
283
+
284
+ @property
285
+ def schema(self) -> JsonSchema:
286
+ """The header schema extracted from its definition."""
287
+ if self._schema is NOT_SET:
288
+ self._schema = self.adapter.extract_header_schema(
289
+ self.definition, self.resolver, self.scope, self.adapter.nullable_keyword
290
+ )
291
+ assert not isinstance(self._schema, NotSet)
292
+ return self._schema
293
+
294
+ @property
295
+ def validator(self) -> Validator:
296
+ """JSON Schema validator for this header."""
297
+ from jsonschema import Draft202012Validator
298
+
299
+ schema = self.schema
300
+ if self._validator is NOT_SET:
301
+ self.adapter.jsonschema_validator_cls.check_schema(schema)
302
+ self._validator = self.adapter.jsonschema_validator_cls(
303
+ schema,
304
+ # Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
305
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
306
+ resolver=RefResolver.from_schema(schema),
307
+ )
308
+ assert not isinstance(self._validator, NotSet)
309
+ return self._validator
310
+
311
+
312
+ def _iter_resolved_headers(
313
+ definition: types.v3.Headers | types.v2.Headers,
314
+ resolver: RefResolver,
315
+ scope: str,
316
+ adapter: SpecificationAdapter,
317
+ ) -> Iterator[tuple[str, OpenApiResponseHeader]]:
318
+ """Iterate and resolve header definitions."""
319
+ for name, header in definition.items():
320
+ new_scope, resolved = maybe_resolve(header, resolver, scope)
321
+ yield (
322
+ name,
323
+ OpenApiResponseHeader(name=name, definition=resolved, resolver=resolver, scope=new_scope, adapter=adapter),
324
+ )
325
+
326
+
327
+ def extract_header_schema_v2(
328
+ header: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
329
+ ) -> JsonSchema:
330
+ return _prepare_schema(cast(dict, header), resolver, scope, nullable_keyword)
331
+
332
+
333
+ def extract_header_schema_v3(
334
+ header: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
335
+ ) -> JsonSchema:
336
+ schema = header.get("schema", {})
337
+ return _prepare_schema(schema, resolver, scope, nullable_keyword)
338
+
339
+
340
+ def iter_response_examples_v2(response: Mapping[str, Any], status_code: str) -> Iterator[tuple[str, object]]:
341
+ # In Swagger 2.0, examples are directly in the response under "examples"
342
+ examples = response.get("examples", {})
343
+ return iter(examples.items())
344
+
345
+
346
+ def iter_response_examples_v3(response: Mapping[str, Any], status_code: str) -> Iterator[tuple[str, object]]:
347
+ for media_type, definition in response.get("content", {}).items():
348
+ # Try to get a more descriptive example name from the `$ref` value
349
+ schema_ref = definition.get("schema", {}).get("$ref")
350
+ if schema_ref:
351
+ name = schema_ref.split("/")[-1]
352
+ else:
353
+ name = f"{status_code}/{media_type}"
354
+
355
+ for examples_container_keyword, example_keyword in (
356
+ ("examples", "example"),
357
+ ("x-examples", "x-example"),
358
+ ):
359
+ examples = definition.get(examples_container_keyword, {})
360
+ if isinstance(examples, dict):
361
+ for example in examples.values():
362
+ if "value" in example:
363
+ yield name, example["value"]
364
+ elif isinstance(examples, list):
365
+ for example in examples:
366
+ yield name, example
367
+ if example_keyword in definition:
368
+ yield name, definition[example_keyword]
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Iterator, Mapping
5
+
6
+ if TYPE_CHECKING:
7
+ from schemathesis.core.compat import RefResolver
8
+ from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
9
+
10
+ ORIGINAL_SECURITY_TYPE_KEY = "x-original-security-type"
11
+
12
+
13
+ @dataclass
14
+ class OpenApiSecurityParameters:
15
+ """Security parameters for an API operation."""
16
+
17
+ _parameters: list[Mapping[str, Any]]
18
+
19
+ __slots__ = ("_parameters",)
20
+
21
+ @classmethod
22
+ def from_definition(
23
+ cls,
24
+ schema: Mapping[str, Any],
25
+ operation: Mapping[str, Any],
26
+ resolver: RefResolver,
27
+ adapter: SpecificationAdapter,
28
+ ) -> OpenApiSecurityParameters:
29
+ return cls(list(adapter.extract_security_parameters(schema, operation, resolver)))
30
+
31
+ def iter_parameters(self) -> Iterator[Mapping[str, Any]]:
32
+ return iter(self._parameters)
33
+
34
+
35
+ def extract_security_parameters_v2(
36
+ schema: Mapping[str, Any], operation: Mapping[str, Any], resolver: RefResolver
37
+ ) -> Iterator[Mapping[str, Any]]:
38
+ """Extract all required security parameters for this operation."""
39
+ defined = extract_security_definitions_v2(schema, resolver)
40
+ required = get_security_requirements(schema, operation)
41
+ optional = has_optional_auth(schema, operation)
42
+
43
+ for key in required:
44
+ if key not in defined:
45
+ continue
46
+ definition = defined[key]
47
+ ty = definition["type"]
48
+
49
+ if ty == "apiKey":
50
+ param = make_api_key_schema(definition, type="string")
51
+ elif ty == "basic":
52
+ parameter_schema = make_auth_header_schema(definition)
53
+ param = make_auth_header(**parameter_schema)
54
+ else:
55
+ continue
56
+
57
+ param[ORIGINAL_SECURITY_TYPE_KEY] = ty
58
+
59
+ if optional:
60
+ param = {**param, "required": False}
61
+
62
+ yield param
63
+
64
+
65
+ def extract_security_parameters_v3(
66
+ schema: Mapping[str, Any],
67
+ operation: Mapping[str, Any],
68
+ resolver: RefResolver,
69
+ ) -> Iterator[Mapping[str, Any]]:
70
+ """Extract all required security parameters for this operation."""
71
+ defined = extract_security_definitions_v3(schema, resolver)
72
+ required = get_security_requirements(schema, operation)
73
+ optional = has_optional_auth(schema, operation)
74
+
75
+ for key in required:
76
+ if key not in defined:
77
+ continue
78
+ definition = defined[key]
79
+ ty = definition["type"]
80
+
81
+ if ty == "apiKey":
82
+ param = make_api_key_schema(definition, schema={"type": "string"})
83
+ elif ty == "http":
84
+ parameter_schema = make_auth_header_schema(definition)
85
+ param = make_auth_header(schema=parameter_schema)
86
+ else:
87
+ continue
88
+
89
+ param[ORIGINAL_SECURITY_TYPE_KEY] = ty
90
+
91
+ if optional:
92
+ param = {**param, "required": False}
93
+
94
+ yield param
95
+
96
+
97
+ def make_auth_header_schema(definition: dict[str, Any]) -> dict[str, str]:
98
+ """Build schema dict for Authorization header based on auth scheme."""
99
+ schema = definition.get("scheme", "basic").lower()
100
+ return {"type": "string", "format": f"_{schema}_auth"}
101
+
102
+
103
+ def make_auth_header(**kwargs: Any) -> dict[str, Any]:
104
+ """Build Authorization header security parameter."""
105
+ return {"name": "Authorization", "in": "header", "required": True, **kwargs}
106
+
107
+
108
+ def make_api_key_schema(definition: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
109
+ """Build API key security parameter from security definition."""
110
+ return {"name": definition["name"], "required": True, "in": definition["in"], **kwargs}
111
+
112
+
113
+ def get_security_requirements(schema: Mapping[str, Any], operation: Mapping[str, Any]) -> list[str]:
114
+ requirements = operation.get("security", schema.get("security", []))
115
+ return [key for requirement in requirements for key in requirement]
116
+
117
+
118
+ def has_optional_auth(schema: Mapping[str, Any], operation: Mapping[str, Any]) -> bool:
119
+ return {} in operation.get("security", schema.get("security", []))
120
+
121
+
122
+ def extract_security_definitions_v2(schema: Mapping[str, Any], resolver: RefResolver) -> Mapping[str, Any]:
123
+ return schema.get("securityDefinitions", {})
124
+
125
+
126
+ def extract_security_definitions_v3(schema: Mapping[str, Any], resolver: RefResolver) -> Mapping[str, Any]:
127
+ """In Open API 3 security definitions are located in ``components`` and may have references inside."""
128
+ components = schema.get("components", {})
129
+ security_schemes = components.get("securitySchemes", {})
130
+ # At this point, the resolution scope could differ from the root scope, that's why we need to restore it
131
+ # as now we resolve root-level references
132
+ if len(resolver._scopes_stack) > 1:
133
+ scope = resolver.resolution_scope
134
+ resolver.pop_scope()
135
+ else:
136
+ scope = None
137
+ resolve = resolver.resolve
138
+ try:
139
+ if "$ref" in security_schemes:
140
+ return resolve(security_schemes["$ref"])[1]
141
+ return {key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()}
142
+ finally:
143
+ if scope is not None:
144
+ resolver._scopes_stack.append(scope)
@@ -0,0 +1,30 @@
1
+ from jsonschema import Draft4Validator
2
+
3
+ from schemathesis.specs.openapi.adapter import parameters, responses, security
4
+ from schemathesis.specs.openapi.adapter.protocol import (
5
+ BuildPathParameter,
6
+ ExtractHeaderSchema,
7
+ ExtractParameterSchema,
8
+ ExtractRawResponseSchema,
9
+ ExtractResponseSchema,
10
+ ExtractSecurityParameters,
11
+ IterParameters,
12
+ IterResponseExamples,
13
+ )
14
+
15
+ nullable_keyword = "x-nullable"
16
+ header_required_keyword = "x-required"
17
+ links_keyword = "x-links"
18
+ example_keyword = "x-example"
19
+ examples_container_keyword = "x-examples"
20
+
21
+ extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v2
22
+ extract_raw_response_schema: ExtractRawResponseSchema = responses.extract_raw_response_schema_v2
23
+ extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v2
24
+ extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v2
25
+ iter_parameters: IterParameters = parameters.iter_parameters_v2
26
+ build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v2
27
+ iter_response_examples: IterResponseExamples = responses.iter_response_examples_v2
28
+ extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v2
29
+
30
+ jsonschema_validator_cls = Draft4Validator
@@ -0,0 +1,30 @@
1
+ from jsonschema import Draft4Validator
2
+
3
+ from schemathesis.specs.openapi.adapter import parameters, responses, security
4
+ from schemathesis.specs.openapi.adapter.protocol import (
5
+ BuildPathParameter,
6
+ ExtractHeaderSchema,
7
+ ExtractParameterSchema,
8
+ ExtractRawResponseSchema,
9
+ ExtractResponseSchema,
10
+ ExtractSecurityParameters,
11
+ IterParameters,
12
+ IterResponseExamples,
13
+ )
14
+
15
+ nullable_keyword = "nullable"
16
+ header_required_keyword = "required"
17
+ links_keyword = "links"
18
+ example_keyword = "example"
19
+ examples_container_keyword = "examples"
20
+
21
+ extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v3
22
+ extract_raw_response_schema: ExtractRawResponseSchema = responses.extract_raw_response_schema_v3
23
+ extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v3
24
+ extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v3
25
+ iter_parameters: IterParameters = parameters.iter_parameters_v3
26
+ build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v3_0
27
+ iter_response_examples: IterResponseExamples = responses.iter_response_examples_v3
28
+ extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v3
29
+
30
+ jsonschema_validator_cls = Draft4Validator
@@ -0,0 +1,30 @@
1
+ from jsonschema import Draft202012Validator
2
+
3
+ from schemathesis.specs.openapi.adapter import parameters, responses, security
4
+ from schemathesis.specs.openapi.adapter.protocol import (
5
+ BuildPathParameter,
6
+ ExtractHeaderSchema,
7
+ ExtractParameterSchema,
8
+ ExtractRawResponseSchema,
9
+ ExtractResponseSchema,
10
+ ExtractSecurityParameters,
11
+ IterParameters,
12
+ IterResponseExamples,
13
+ )
14
+
15
+ nullable_keyword = "nullable"
16
+ header_required_keyword = "required"
17
+ links_keyword = "links"
18
+ example_keyword = "example"
19
+ examples_container_keyword = "examples"
20
+
21
+ extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v3
22
+ extract_raw_response_schema: ExtractRawResponseSchema = responses.extract_raw_response_schema_v3
23
+ extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v3
24
+ extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v3
25
+ iter_parameters: IterParameters = parameters.iter_parameters_v3
26
+ build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v3_1
27
+ iter_response_examples: IterResponseExamples = responses.iter_response_examples_v3
28
+ extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v3
29
+
30
+ jsonschema_validator_cls = Draft202012Validator