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