schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,708 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import json
5
- import pathlib
6
- import re
7
- from typing import IO, TYPE_CHECKING, Any, Callable, cast
8
- from urllib.parse import urljoin
9
-
10
- from ... import experimental, fixups
11
- from ...code_samples import CodeSampleStyle
12
- from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
13
- from ...exceptions import SchemaError, SchemaErrorType
14
- from ...filters import filter_set_from_components
15
- from ...generation import (
16
- DEFAULT_DATA_GENERATION_METHODS,
17
- DataGenerationMethod,
18
- DataGenerationMethodInput,
19
- GenerationConfig,
20
- )
21
- from ...hooks import HookContext, dispatch
22
- from ...internal.deprecation import warn_filtration_arguments
23
- from ...internal.output import OutputConfig
24
- from ...internal.validation import require_relative_url
25
- from ...loaders import load_schema_from_url, load_yaml
26
- from ...throttling import build_limiter
27
- from ...transports.content_types import is_json_media_type, is_yaml_media_type
28
- from ...transports.headers import setup_default_headers
29
- from ...types import Filter, NotSet, PathLike, Specification
30
- from . import definitions, validation
31
-
32
- if TYPE_CHECKING:
33
- import jsonschema
34
- from pyrate_limiter import Limiter
35
-
36
- from ...lazy import LazySchema
37
- from ...transports.responses import GenericResponse
38
- from .schemas import BaseOpenAPISchema
39
-
40
-
41
- def _is_json_response(response: GenericResponse) -> bool:
42
- """Guess if the response contains JSON."""
43
- content_type = response.headers.get("Content-Type")
44
- if content_type is not None:
45
- return is_json_media_type(content_type)
46
- return False
47
-
48
-
49
- def _has_suffix(path: PathLike, suffix: str) -> bool:
50
- if isinstance(path, str):
51
- return path.endswith(suffix)
52
- return path.suffix == suffix
53
-
54
-
55
- def _is_json_path(path: PathLike) -> bool:
56
- return _has_suffix(path, ".json")
57
-
58
-
59
- def _is_yaml_response(response: GenericResponse) -> bool:
60
- """Guess if the response contains YAML."""
61
- content_type = response.headers.get("Content-Type")
62
- if content_type is not None:
63
- return is_yaml_media_type(content_type)
64
- return False
65
-
66
-
67
- def _is_yaml_path(path: PathLike) -> bool:
68
- return _has_suffix(path, ".yaml") or _has_suffix(path, ".yml")
69
-
70
-
71
- def from_path(
72
- path: PathLike,
73
- *,
74
- app: Any = None,
75
- base_url: str | None = None,
76
- method: Filter | None = None,
77
- endpoint: Filter | None = None,
78
- tag: Filter | None = None,
79
- operation_id: Filter | None = None,
80
- skip_deprecated_operations: bool | None = None,
81
- validate_schema: bool = False,
82
- force_schema_version: str | None = None,
83
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
84
- generation_config: GenerationConfig | None = None,
85
- output_config: OutputConfig | None = None,
86
- code_sample_style: str = CodeSampleStyle.default().name,
87
- rate_limit: str | None = None,
88
- encoding: str = "utf8",
89
- sanitize_output: bool = True,
90
- ) -> BaseOpenAPISchema:
91
- """Load Open API schema via a file from an OS path.
92
-
93
- :param path: A path to the schema file.
94
- :param encoding: The name of the encoding used to decode the file.
95
- """
96
- with open(path, encoding=encoding) as fd:
97
- return from_file(
98
- fd,
99
- app=app,
100
- base_url=base_url,
101
- method=method,
102
- endpoint=endpoint,
103
- tag=tag,
104
- operation_id=operation_id,
105
- skip_deprecated_operations=skip_deprecated_operations,
106
- validate_schema=validate_schema,
107
- force_schema_version=force_schema_version,
108
- data_generation_methods=data_generation_methods,
109
- generation_config=generation_config,
110
- output_config=output_config,
111
- code_sample_style=code_sample_style,
112
- location=pathlib.Path(path).absolute().as_uri(),
113
- rate_limit=rate_limit,
114
- sanitize_output=sanitize_output,
115
- __expects_json=_is_json_path(path),
116
- __expects_yaml=_is_yaml_path(path),
117
- )
118
-
119
-
120
- def from_uri(
121
- uri: str,
122
- *,
123
- app: Any = None,
124
- base_url: str | None = None,
125
- port: int | None = None,
126
- method: Filter | None = None,
127
- endpoint: Filter | None = None,
128
- tag: Filter | None = None,
129
- operation_id: Filter | None = None,
130
- skip_deprecated_operations: bool | None = None,
131
- validate_schema: bool = False,
132
- force_schema_version: str | None = None,
133
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
134
- generation_config: GenerationConfig | None = None,
135
- output_config: OutputConfig | None = None,
136
- code_sample_style: str = CodeSampleStyle.default().name,
137
- wait_for_schema: float | None = None,
138
- rate_limit: str | None = None,
139
- sanitize_output: bool = True,
140
- **kwargs: Any,
141
- ) -> BaseOpenAPISchema:
142
- """Load Open API schema from the network.
143
-
144
- :param str uri: Schema URL.
145
- """
146
- import backoff
147
- import requests
148
-
149
- setup_default_headers(kwargs)
150
- if port:
151
- from yarl import URL
152
-
153
- uri = str(URL(uri).with_port(port))
154
- if not base_url:
155
- base_url = uri
156
-
157
- if wait_for_schema is not None:
158
-
159
- @backoff.on_exception( # type: ignore
160
- backoff.constant,
161
- requests.exceptions.ConnectionError,
162
- max_time=wait_for_schema,
163
- interval=WAIT_FOR_SCHEMA_INTERVAL,
164
- )
165
- def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
166
- return requests.get(_uri, **_kwargs)
167
-
168
- else:
169
- _load_schema = requests.get
170
-
171
- kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
172
- response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
173
- return from_file(
174
- response.text,
175
- app=app,
176
- base_url=base_url,
177
- method=method,
178
- endpoint=endpoint,
179
- tag=tag,
180
- operation_id=operation_id,
181
- skip_deprecated_operations=skip_deprecated_operations,
182
- validate_schema=validate_schema,
183
- force_schema_version=force_schema_version,
184
- data_generation_methods=data_generation_methods,
185
- generation_config=generation_config,
186
- output_config=output_config,
187
- code_sample_style=code_sample_style,
188
- location=uri,
189
- rate_limit=rate_limit,
190
- sanitize_output=sanitize_output,
191
- __expects_json=_is_json_response(response),
192
- __expects_yaml=_is_yaml_response(response),
193
- )
194
-
195
-
196
- SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
197
- SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
198
- SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
199
-
200
-
201
- def _load_yaml(data: str, include_details_on_error: bool = False) -> dict[str, Any]:
202
- import yaml
203
-
204
- try:
205
- return load_yaml(data)
206
- except yaml.YAMLError as exc:
207
- if include_details_on_error:
208
- type_ = SchemaErrorType.SYNTAX_ERROR
209
- message = SCHEMA_SYNTAX_ERROR
210
- extras = [entry for entry in str(exc).splitlines() if entry]
211
- else:
212
- type_ = SchemaErrorType.UNEXPECTED_CONTENT_TYPE
213
- message = SCHEMA_LOADING_ERROR
214
- extras = []
215
- raise SchemaError(type_, message, extras=extras) from exc
216
-
217
-
218
- def from_file(
219
- file: IO[str] | str,
220
- *,
221
- app: Any = None,
222
- base_url: str | None = None,
223
- method: Filter | None = None,
224
- endpoint: Filter | None = None,
225
- tag: Filter | None = None,
226
- operation_id: Filter | None = None,
227
- skip_deprecated_operations: bool | None = None,
228
- validate_schema: bool = False,
229
- force_schema_version: str | None = None,
230
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
231
- generation_config: GenerationConfig | None = None,
232
- output_config: OutputConfig | None = None,
233
- code_sample_style: str = CodeSampleStyle.default().name,
234
- location: str | None = None,
235
- rate_limit: str | None = None,
236
- sanitize_output: bool = True,
237
- __expects_json: bool = False,
238
- __expects_yaml: bool = False,
239
- **kwargs: Any, # needed in the runner to have compatible API across all loaders
240
- ) -> BaseOpenAPISchema:
241
- """Load Open API schema from a file descriptor, string or bytes.
242
-
243
- :param file: Could be a file descriptor, string or bytes.
244
- """
245
- if hasattr(file, "read"):
246
- data = file.read() # type: ignore
247
- else:
248
- data = file
249
- if __expects_json:
250
- try:
251
- raw = json.loads(data)
252
- except json.JSONDecodeError as exc:
253
- # Fallback to a slower YAML loader. This way we'll still load schemas from responses with
254
- # invalid `Content-Type` headers or YAML files that have the `.json` extension.
255
- # This is a rare case, and it will be slower but trying JSON first improves a more common use case
256
- try:
257
- raw = _load_yaml(data)
258
- except SchemaError:
259
- raise SchemaError(
260
- SchemaErrorType.SYNTAX_ERROR,
261
- SCHEMA_SYNTAX_ERROR,
262
- extras=[entry for entry in str(exc).splitlines() if entry],
263
- ) from exc
264
- else:
265
- raw = _load_yaml(data, include_details_on_error=__expects_yaml)
266
- return from_dict(
267
- raw,
268
- app=app,
269
- base_url=base_url,
270
- method=method,
271
- endpoint=endpoint,
272
- tag=tag,
273
- operation_id=operation_id,
274
- skip_deprecated_operations=skip_deprecated_operations,
275
- validate_schema=validate_schema,
276
- force_schema_version=force_schema_version,
277
- data_generation_methods=data_generation_methods,
278
- generation_config=generation_config,
279
- output_config=output_config,
280
- code_sample_style=code_sample_style,
281
- location=location,
282
- rate_limit=rate_limit,
283
- sanitize_output=sanitize_output,
284
- )
285
-
286
-
287
- def _is_fast_api(app: Any) -> bool:
288
- for cls in app.__class__.__mro__:
289
- if f"{cls.__module__}.{cls.__qualname__}" == "fastapi.applications.FastAPI":
290
- return True
291
- return False
292
-
293
-
294
- def from_dict(
295
- raw_schema: dict[str, Any],
296
- *,
297
- app: Any = None,
298
- base_url: str | None = None,
299
- method: Filter | None = None,
300
- endpoint: Filter | None = None,
301
- tag: Filter | None = None,
302
- operation_id: Filter | None = None,
303
- skip_deprecated_operations: bool | None = None,
304
- validate_schema: bool = False,
305
- force_schema_version: str | None = None,
306
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
307
- generation_config: GenerationConfig | None = None,
308
- output_config: OutputConfig | None = None,
309
- code_sample_style: str = CodeSampleStyle.default().name,
310
- location: str | None = None,
311
- rate_limit: str | None = None,
312
- sanitize_output: bool = True,
313
- ) -> BaseOpenAPISchema:
314
- """Load Open API schema from a Python dictionary.
315
-
316
- :param dict raw_schema: A schema to load.
317
- """
318
- from ... import transports
319
- from .schemas import OpenApi30, SwaggerV20
320
-
321
- if not isinstance(raw_schema, dict):
322
- raise SchemaError(SchemaErrorType.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
323
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
324
- hook_context = HookContext()
325
- is_openapi_31 = raw_schema.get("openapi", "").startswith("3.1")
326
- is_fast_api_fixup_installed = fixups.is_installed("fast_api")
327
- if is_fast_api_fixup_installed and is_openapi_31:
328
- fixups.fast_api.uninstall()
329
- elif _is_fast_api(app):
330
- fixups.fast_api.adjust_schema(raw_schema)
331
- dispatch("before_load_schema", hook_context, raw_schema)
332
- rate_limiter: Limiter | None = None
333
- if rate_limit is not None:
334
- rate_limiter = build_limiter(rate_limit)
335
-
336
- for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
337
- value = locals()[name]
338
- if value is not None:
339
- warn_filtration_arguments(name)
340
- filter_set = filter_set_from_components(
341
- include=True,
342
- method=method,
343
- endpoint=endpoint,
344
- tag=tag,
345
- operation_id=operation_id,
346
- skip_deprecated_operations=skip_deprecated_operations,
347
- )
348
-
349
- def init_openapi_2() -> SwaggerV20:
350
- _maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
351
- instance = SwaggerV20(
352
- raw_schema,
353
- specification=Specification.OPENAPI,
354
- app=app,
355
- base_url=base_url,
356
- filter_set=filter_set,
357
- validate_schema=validate_schema,
358
- data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
359
- generation_config=generation_config or GenerationConfig(),
360
- output_config=output_config or OutputConfig(),
361
- code_sample_style=_code_sample_style,
362
- location=location,
363
- rate_limiter=rate_limiter,
364
- sanitize_output=sanitize_output,
365
- transport=transports.get(app),
366
- )
367
- dispatch("after_load_schema", hook_context, instance)
368
- return instance
369
-
370
- def init_openapi_3(forced: bool) -> OpenApi30:
371
- version = raw_schema["openapi"]
372
- if (
373
- not (is_openapi_31 and experimental.OPEN_API_3_1.is_enabled)
374
- and not forced
375
- and not OPENAPI_30_VERSION_RE.match(version)
376
- ):
377
- if is_openapi_31:
378
- raise SchemaError(
379
- SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION,
380
- f"The provided schema uses Open API {version}, which is currently not fully supported.",
381
- )
382
- raise SchemaError(
383
- SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
384
- f"The provided schema uses Open API {version}, which is currently not supported.",
385
- )
386
- if is_openapi_31:
387
- validator = definitions.OPENAPI_31_VALIDATOR
388
- else:
389
- validator = definitions.OPENAPI_30_VALIDATOR
390
- _maybe_validate_schema(raw_schema, validator, validate_schema)
391
- instance = OpenApi30(
392
- raw_schema,
393
- specification=Specification.OPENAPI,
394
- app=app,
395
- base_url=base_url,
396
- filter_set=filter_set,
397
- validate_schema=validate_schema,
398
- data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
399
- generation_config=generation_config or GenerationConfig(),
400
- output_config=output_config or OutputConfig(),
401
- code_sample_style=_code_sample_style,
402
- location=location,
403
- rate_limiter=rate_limiter,
404
- sanitize_output=sanitize_output,
405
- transport=transports.get(app),
406
- )
407
- dispatch("after_load_schema", hook_context, instance)
408
- return instance
409
-
410
- if force_schema_version == "20":
411
- return init_openapi_2()
412
- if force_schema_version == "30":
413
- return init_openapi_3(forced=True)
414
- if "swagger" in raw_schema:
415
- return init_openapi_2()
416
- if "openapi" in raw_schema:
417
- return init_openapi_3(forced=False)
418
- raise SchemaError(
419
- SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
420
- "Unable to determine the Open API version as it's not specified in the document.",
421
- )
422
-
423
-
424
- OPENAPI_30_VERSION_RE = re.compile(r"^3\.0\.\d(-.+)?$")
425
-
426
- # It is a common case when API schemas are stored in the YAML format and HTTP status codes are numbers
427
- # The Open API spec requires HTTP status codes as strings
428
- DOC_ENTRY = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#patterned-fields-1"
429
- NUMERIC_STATUS_CODES_MESSAGE = f"""Numeric HTTP status codes detected in your YAML schema.
430
- According to the Open API specification, status codes must be strings, not numbers.
431
- For more details, check the Open API documentation: {DOC_ENTRY}
432
-
433
- Please, stringify the following status codes:"""
434
- NON_STRING_OBJECT_KEY_MESSAGE = (
435
- "The Open API specification requires all keys in the schema to be strings. "
436
- "You have some keys that are not strings."
437
- )
438
-
439
-
440
- def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str:
441
- buffer = io.StringIO()
442
- for status_code, path in status_codes:
443
- buffer.write(f" - {status_code} at schema['paths']")
444
- for chunk in path:
445
- buffer.write(f"[{chunk!r}]")
446
- buffer.write("['responses']\n")
447
- return buffer.getvalue().rstrip()
448
-
449
-
450
- def _maybe_validate_schema(
451
- instance: dict[str, Any], validator: jsonschema.validators.Draft4Validator, validate_schema: bool
452
- ) -> None:
453
- from jsonschema import ValidationError
454
-
455
- if validate_schema:
456
- try:
457
- validator.validate(instance)
458
- except TypeError as exc:
459
- if validation.is_pattern_error(exc):
460
- status_codes = validation.find_numeric_http_status_codes(instance)
461
- if status_codes:
462
- message = _format_status_codes(status_codes)
463
- raise SchemaError(
464
- SchemaErrorType.YAML_NUMERIC_STATUS_CODES, f"{NUMERIC_STATUS_CODES_MESSAGE}\n{message}"
465
- ) from exc
466
- # Some other pattern error
467
- raise SchemaError(SchemaErrorType.YAML_NON_STRING_KEYS, NON_STRING_OBJECT_KEY_MESSAGE) from exc
468
- raise SchemaError(SchemaErrorType.UNCLASSIFIED, "Unknown error") from exc
469
- except ValidationError as exc:
470
- raise SchemaError(
471
- SchemaErrorType.OPEN_API_INVALID_SCHEMA,
472
- SCHEMA_INVALID_ERROR,
473
- extras=[entry for entry in str(exc).splitlines() if entry],
474
- ) from exc
475
-
476
-
477
- def from_pytest_fixture(
478
- fixture_name: str,
479
- *,
480
- app: Any = NOT_SET,
481
- base_url: str | None | NotSet = NOT_SET,
482
- method: Filter | None = NOT_SET,
483
- endpoint: Filter | None = NOT_SET,
484
- tag: Filter | None = NOT_SET,
485
- operation_id: Filter | None = NOT_SET,
486
- skip_deprecated_operations: bool | None = None,
487
- validate_schema: bool = False,
488
- data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
489
- generation_config: GenerationConfig | NotSet = NOT_SET,
490
- output_config: OutputConfig | NotSet = NOT_SET,
491
- code_sample_style: str = CodeSampleStyle.default().name,
492
- rate_limit: str | None = None,
493
- sanitize_output: bool = True,
494
- ) -> LazySchema:
495
- """Load schema from a ``pytest`` fixture.
496
-
497
- It is useful if you don't want to make network requests during module loading. With this loader you can defer it
498
- to a fixture.
499
-
500
- Note, the fixture should return a ``BaseSchema`` instance loaded with another loader.
501
-
502
- :param str fixture_name: The name of a fixture to load.
503
- """
504
- from ...lazy import LazySchema
505
-
506
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
507
- _data_generation_methods: DataGenerationMethodInput | NotSet
508
- if data_generation_methods is not NOT_SET:
509
- data_generation_methods = cast(DataGenerationMethodInput, data_generation_methods)
510
- _data_generation_methods = DataGenerationMethod.ensure_list(data_generation_methods)
511
- else:
512
- _data_generation_methods = data_generation_methods
513
- rate_limiter: Limiter | None = None
514
- if rate_limit is not None:
515
- rate_limiter = build_limiter(rate_limit)
516
- for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
517
- value = locals()[name]
518
- if value is not None:
519
- warn_filtration_arguments(name)
520
- filter_set = filter_set_from_components(
521
- include=True,
522
- method=method,
523
- endpoint=endpoint,
524
- tag=tag,
525
- operation_id=operation_id,
526
- skip_deprecated_operations=skip_deprecated_operations,
527
- )
528
- return LazySchema(
529
- fixture_name,
530
- app=app,
531
- base_url=base_url,
532
- filter_set=filter_set,
533
- validate_schema=validate_schema,
534
- data_generation_methods=_data_generation_methods,
535
- generation_config=generation_config,
536
- output_config=output_config,
537
- code_sample_style=_code_sample_style,
538
- rate_limiter=rate_limiter,
539
- sanitize_output=sanitize_output,
540
- )
541
-
542
-
543
- def from_wsgi(
544
- schema_path: str,
545
- app: Any,
546
- *,
547
- base_url: str | None = None,
548
- method: Filter | None = None,
549
- endpoint: Filter | None = None,
550
- tag: Filter | None = None,
551
- operation_id: Filter | None = None,
552
- skip_deprecated_operations: bool | None = None,
553
- validate_schema: bool = False,
554
- force_schema_version: str | None = None,
555
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
556
- generation_config: GenerationConfig | None = None,
557
- output_config: OutputConfig | None = None,
558
- code_sample_style: str = CodeSampleStyle.default().name,
559
- rate_limit: str | None = None,
560
- sanitize_output: bool = True,
561
- **kwargs: Any,
562
- ) -> BaseOpenAPISchema:
563
- """Load Open API schema from a WSGI app.
564
-
565
- :param str schema_path: An in-app relative URL to the schema.
566
- :param app: A WSGI app instance.
567
- """
568
- from werkzeug.test import Client
569
-
570
- from ...transports.responses import WSGIResponse
571
-
572
- require_relative_url(schema_path)
573
- setup_default_headers(kwargs)
574
- client = Client(app, WSGIResponse)
575
- response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
576
- return from_file(
577
- response.data,
578
- app=app,
579
- base_url=base_url,
580
- method=method,
581
- endpoint=endpoint,
582
- tag=tag,
583
- operation_id=operation_id,
584
- skip_deprecated_operations=skip_deprecated_operations,
585
- validate_schema=validate_schema,
586
- force_schema_version=force_schema_version,
587
- data_generation_methods=data_generation_methods,
588
- generation_config=generation_config,
589
- output_config=output_config,
590
- code_sample_style=code_sample_style,
591
- location=schema_path,
592
- rate_limit=rate_limit,
593
- sanitize_output=sanitize_output,
594
- __expects_json=_is_json_response(response),
595
- )
596
-
597
-
598
- def get_loader_for_app(app: Any) -> Callable:
599
- from ...transports.asgi import is_asgi_app
600
-
601
- if is_asgi_app(app):
602
- return from_asgi
603
- if app.__class__.__module__.startswith("aiohttp."):
604
- return from_aiohttp
605
- return from_wsgi
606
-
607
-
608
- def from_aiohttp(
609
- schema_path: str,
610
- app: Any,
611
- *,
612
- base_url: str | None = None,
613
- method: Filter | None = None,
614
- endpoint: Filter | None = None,
615
- tag: Filter | None = None,
616
- operation_id: Filter | None = None,
617
- skip_deprecated_operations: bool | None = None,
618
- validate_schema: bool = False,
619
- force_schema_version: str | None = None,
620
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
621
- generation_config: GenerationConfig | None = None,
622
- output_config: OutputConfig | None = None,
623
- code_sample_style: str = CodeSampleStyle.default().name,
624
- rate_limit: str | None = None,
625
- sanitize_output: bool = True,
626
- **kwargs: Any,
627
- ) -> BaseOpenAPISchema:
628
- """Load Open API schema from an AioHTTP app.
629
-
630
- :param str schema_path: An in-app relative URL to the schema.
631
- :param app: An AioHTTP app instance.
632
- """
633
- from ...extra._aiohttp import run_server
634
-
635
- port = run_server(app)
636
- app_url = f"http://127.0.0.1:{port}/"
637
- url = urljoin(app_url, schema_path)
638
- return from_uri(
639
- url,
640
- base_url=base_url,
641
- method=method,
642
- endpoint=endpoint,
643
- tag=tag,
644
- operation_id=operation_id,
645
- skip_deprecated_operations=skip_deprecated_operations,
646
- validate_schema=validate_schema,
647
- force_schema_version=force_schema_version,
648
- data_generation_methods=data_generation_methods,
649
- generation_config=generation_config,
650
- output_config=output_config,
651
- code_sample_style=code_sample_style,
652
- rate_limit=rate_limit,
653
- sanitize_output=sanitize_output,
654
- **kwargs,
655
- )
656
-
657
-
658
- def from_asgi(
659
- schema_path: str,
660
- app: Any,
661
- *,
662
- base_url: str | None = None,
663
- method: Filter | None = None,
664
- endpoint: Filter | None = None,
665
- tag: Filter | None = None,
666
- operation_id: Filter | None = None,
667
- skip_deprecated_operations: bool | None = None,
668
- validate_schema: bool = False,
669
- force_schema_version: str | None = None,
670
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
671
- generation_config: GenerationConfig | None = None,
672
- output_config: OutputConfig | None = None,
673
- code_sample_style: str = CodeSampleStyle.default().name,
674
- rate_limit: str | None = None,
675
- sanitize_output: bool = True,
676
- **kwargs: Any,
677
- ) -> BaseOpenAPISchema:
678
- """Load Open API schema from an ASGI app.
679
-
680
- :param str schema_path: An in-app relative URL to the schema.
681
- :param app: An ASGI app instance.
682
- """
683
- from starlette_testclient import TestClient as ASGIClient
684
-
685
- require_relative_url(schema_path)
686
- setup_default_headers(kwargs)
687
- client = ASGIClient(app)
688
- response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
689
- return from_file(
690
- response.text,
691
- app=app,
692
- base_url=base_url,
693
- method=method,
694
- endpoint=endpoint,
695
- tag=tag,
696
- operation_id=operation_id,
697
- skip_deprecated_operations=skip_deprecated_operations,
698
- validate_schema=validate_schema,
699
- force_schema_version=force_schema_version,
700
- data_generation_methods=data_generation_methods,
701
- generation_config=generation_config,
702
- output_config=output_config,
703
- code_sample_style=code_sample_style,
704
- location=schema_path,
705
- rate_limit=rate_limit,
706
- sanitize_output=sanitize_output,
707
- __expects_json=_is_json_response(response),
708
- )