schemathesis 3.21.2__py3-none-any.whl → 3.22.1__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.
- schemathesis/__init__.py +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,19 +3,24 @@ import io
|
|
|
3
3
|
import json
|
|
4
4
|
import pathlib
|
|
5
5
|
import re
|
|
6
|
-
from typing import IO, Any, Callable,
|
|
6
|
+
from typing import IO, Any, Callable, cast, TYPE_CHECKING
|
|
7
7
|
from urllib.parse import urljoin
|
|
8
8
|
|
|
9
9
|
from ... import experimental, fixups
|
|
10
10
|
from ...code_samples import CodeSampleStyle
|
|
11
|
-
from ...generation import
|
|
11
|
+
from ...generation import (
|
|
12
|
+
DEFAULT_DATA_GENERATION_METHODS,
|
|
13
|
+
DataGenerationMethodInput,
|
|
14
|
+
DataGenerationMethod,
|
|
15
|
+
GenerationConfig,
|
|
16
|
+
)
|
|
12
17
|
from ...constants import WAIT_FOR_SCHEMA_INTERVAL
|
|
13
18
|
from ...exceptions import SchemaError, SchemaErrorType
|
|
14
19
|
from ...hooks import HookContext, dispatch
|
|
15
20
|
from ...loaders import load_schema_from_url, load_yaml
|
|
16
21
|
from ...throttling import build_limiter
|
|
17
22
|
from ...types import Filter, NotSet, PathLike
|
|
18
|
-
from ...transports.content_types import is_json_media_type
|
|
23
|
+
from ...transports.content_types import is_json_media_type, is_yaml_media_type
|
|
19
24
|
from ...transports.headers import setup_default_headers
|
|
20
25
|
from ...internal.validation import require_relative_url
|
|
21
26
|
from ...constants import NOT_SET
|
|
@@ -37,27 +42,44 @@ def _is_json_response(response: GenericResponse) -> bool:
|
|
|
37
42
|
return False
|
|
38
43
|
|
|
39
44
|
|
|
40
|
-
def
|
|
45
|
+
def _has_suffix(path: PathLike, suffix: str) -> bool:
|
|
41
46
|
if isinstance(path, str):
|
|
42
|
-
return path.endswith(
|
|
43
|
-
return path.suffix ==
|
|
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")
|
|
44
65
|
|
|
45
66
|
|
|
46
67
|
def from_path(
|
|
47
68
|
path: PathLike,
|
|
48
69
|
*,
|
|
49
70
|
app: Any = None,
|
|
50
|
-
base_url:
|
|
51
|
-
method:
|
|
52
|
-
endpoint:
|
|
53
|
-
tag:
|
|
54
|
-
operation_id:
|
|
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,
|
|
55
76
|
skip_deprecated_operations: bool = False,
|
|
56
77
|
validate_schema: bool = False,
|
|
57
|
-
force_schema_version:
|
|
78
|
+
force_schema_version: str | None = None,
|
|
58
79
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
80
|
+
generation_config: GenerationConfig | None = None,
|
|
59
81
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
60
|
-
rate_limit:
|
|
82
|
+
rate_limit: str | None = None,
|
|
61
83
|
encoding: str = "utf8",
|
|
62
84
|
sanitize_output: bool = True,
|
|
63
85
|
) -> BaseOpenAPISchema:
|
|
@@ -79,11 +101,13 @@ def from_path(
|
|
|
79
101
|
validate_schema=validate_schema,
|
|
80
102
|
force_schema_version=force_schema_version,
|
|
81
103
|
data_generation_methods=data_generation_methods,
|
|
104
|
+
generation_config=generation_config,
|
|
82
105
|
code_sample_style=code_sample_style,
|
|
83
106
|
location=pathlib.Path(path).absolute().as_uri(),
|
|
84
107
|
rate_limit=rate_limit,
|
|
85
108
|
sanitize_output=sanitize_output,
|
|
86
109
|
__expects_json=_is_json_path(path),
|
|
110
|
+
__expects_yaml=_is_yaml_path(path),
|
|
87
111
|
)
|
|
88
112
|
|
|
89
113
|
|
|
@@ -91,19 +115,20 @@ def from_uri(
|
|
|
91
115
|
uri: str,
|
|
92
116
|
*,
|
|
93
117
|
app: Any = None,
|
|
94
|
-
base_url:
|
|
95
|
-
port:
|
|
96
|
-
method:
|
|
97
|
-
endpoint:
|
|
98
|
-
tag:
|
|
99
|
-
operation_id:
|
|
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,
|
|
100
124
|
skip_deprecated_operations: bool = False,
|
|
101
125
|
validate_schema: bool = False,
|
|
102
|
-
force_schema_version:
|
|
126
|
+
force_schema_version: str | None = None,
|
|
103
127
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
128
|
+
generation_config: GenerationConfig | None = None,
|
|
104
129
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
105
|
-
wait_for_schema:
|
|
106
|
-
rate_limit:
|
|
130
|
+
wait_for_schema: float | None = None,
|
|
131
|
+
rate_limit: str | None = None,
|
|
107
132
|
sanitize_output: bool = True,
|
|
108
133
|
**kwargs: Any,
|
|
109
134
|
) -> BaseOpenAPISchema:
|
|
@@ -149,44 +174,57 @@ def from_uri(
|
|
|
149
174
|
validate_schema=validate_schema,
|
|
150
175
|
force_schema_version=force_schema_version,
|
|
151
176
|
data_generation_methods=data_generation_methods,
|
|
177
|
+
generation_config=generation_config,
|
|
152
178
|
code_sample_style=code_sample_style,
|
|
153
179
|
location=uri,
|
|
154
180
|
rate_limit=rate_limit,
|
|
155
181
|
sanitize_output=sanitize_output,
|
|
156
182
|
__expects_json=_is_json_response(response),
|
|
183
|
+
__expects_yaml=_is_yaml_response(response),
|
|
157
184
|
)
|
|
158
185
|
|
|
159
186
|
|
|
160
187
|
SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
|
|
188
|
+
SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
|
|
161
189
|
|
|
162
190
|
|
|
163
|
-
def _load_yaml(data: str) ->
|
|
191
|
+
def _load_yaml(data: str, include_details_on_error: bool = False) -> dict[str, Any]:
|
|
164
192
|
import yaml
|
|
165
193
|
|
|
166
194
|
try:
|
|
167
195
|
return load_yaml(data)
|
|
168
196
|
except yaml.YAMLError as exc:
|
|
169
|
-
|
|
197
|
+
if include_details_on_error:
|
|
198
|
+
type_ = SchemaErrorType.SYNTAX_ERROR
|
|
199
|
+
message = SCHEMA_SYNTAX_ERROR
|
|
200
|
+
extras = [entry for entry in str(exc).splitlines() if entry]
|
|
201
|
+
else:
|
|
202
|
+
type_ = SchemaErrorType.UNEXPECTED_CONTENT_TYPE
|
|
203
|
+
message = SCHEMA_LOADING_ERROR
|
|
204
|
+
extras = []
|
|
205
|
+
raise SchemaError(type_, message, extras=extras) from exc
|
|
170
206
|
|
|
171
207
|
|
|
172
208
|
def from_file(
|
|
173
|
-
file:
|
|
209
|
+
file: IO[str] | str,
|
|
174
210
|
*,
|
|
175
211
|
app: Any = None,
|
|
176
|
-
base_url:
|
|
177
|
-
method:
|
|
178
|
-
endpoint:
|
|
179
|
-
tag:
|
|
180
|
-
operation_id:
|
|
212
|
+
base_url: str | None = None,
|
|
213
|
+
method: Filter | None = None,
|
|
214
|
+
endpoint: Filter | None = None,
|
|
215
|
+
tag: Filter | None = None,
|
|
216
|
+
operation_id: Filter | None = None,
|
|
181
217
|
skip_deprecated_operations: bool = False,
|
|
182
218
|
validate_schema: bool = False,
|
|
183
|
-
force_schema_version:
|
|
219
|
+
force_schema_version: str | None = None,
|
|
184
220
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
221
|
+
generation_config: GenerationConfig | None = None,
|
|
185
222
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
186
|
-
location:
|
|
187
|
-
rate_limit:
|
|
223
|
+
location: str | None = None,
|
|
224
|
+
rate_limit: str | None = None,
|
|
188
225
|
sanitize_output: bool = True,
|
|
189
226
|
__expects_json: bool = False,
|
|
227
|
+
__expects_yaml: bool = False,
|
|
190
228
|
**kwargs: Any, # needed in the runner to have compatible API across all loaders
|
|
191
229
|
) -> BaseOpenAPISchema:
|
|
192
230
|
"""Load Open API schema from a file descriptor, string or bytes.
|
|
@@ -200,13 +238,20 @@ def from_file(
|
|
|
200
238
|
if __expects_json:
|
|
201
239
|
try:
|
|
202
240
|
raw = json.loads(data)
|
|
203
|
-
except json.JSONDecodeError:
|
|
241
|
+
except json.JSONDecodeError as exc:
|
|
204
242
|
# Fallback to a slower YAML loader. This way we'll still load schemas from responses with
|
|
205
243
|
# invalid `Content-Type` headers or YAML files that have the `.json` extension.
|
|
206
244
|
# This is a rare case, and it will be slower but trying JSON first improves a more common use case
|
|
207
|
-
|
|
245
|
+
try:
|
|
246
|
+
raw = _load_yaml(data)
|
|
247
|
+
except SchemaError:
|
|
248
|
+
raise SchemaError(
|
|
249
|
+
SchemaErrorType.SYNTAX_ERROR,
|
|
250
|
+
SCHEMA_SYNTAX_ERROR,
|
|
251
|
+
extras=[entry for entry in str(exc).splitlines() if entry],
|
|
252
|
+
) from exc
|
|
208
253
|
else:
|
|
209
|
-
raw = _load_yaml(data)
|
|
254
|
+
raw = _load_yaml(data, include_details_on_error=__expects_yaml)
|
|
210
255
|
return from_dict(
|
|
211
256
|
raw,
|
|
212
257
|
app=app,
|
|
@@ -219,6 +264,7 @@ def from_file(
|
|
|
219
264
|
validate_schema=validate_schema,
|
|
220
265
|
force_schema_version=force_schema_version,
|
|
221
266
|
data_generation_methods=data_generation_methods,
|
|
267
|
+
generation_config=generation_config,
|
|
222
268
|
code_sample_style=code_sample_style,
|
|
223
269
|
location=location,
|
|
224
270
|
rate_limit=rate_limit,
|
|
@@ -234,21 +280,22 @@ def _is_fast_api(app: Any) -> bool:
|
|
|
234
280
|
|
|
235
281
|
|
|
236
282
|
def from_dict(
|
|
237
|
-
raw_schema:
|
|
283
|
+
raw_schema: dict[str, Any],
|
|
238
284
|
*,
|
|
239
285
|
app: Any = None,
|
|
240
|
-
base_url:
|
|
241
|
-
method:
|
|
242
|
-
endpoint:
|
|
243
|
-
tag:
|
|
244
|
-
operation_id:
|
|
286
|
+
base_url: str | None = None,
|
|
287
|
+
method: Filter | None = None,
|
|
288
|
+
endpoint: Filter | None = None,
|
|
289
|
+
tag: Filter | None = None,
|
|
290
|
+
operation_id: Filter | None = None,
|
|
245
291
|
skip_deprecated_operations: bool = False,
|
|
246
292
|
validate_schema: bool = False,
|
|
247
|
-
force_schema_version:
|
|
293
|
+
force_schema_version: str | None = None,
|
|
248
294
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
295
|
+
generation_config: GenerationConfig | None = None,
|
|
249
296
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
250
|
-
location:
|
|
251
|
-
rate_limit:
|
|
297
|
+
location: str | None = None,
|
|
298
|
+
rate_limit: str | None = None,
|
|
252
299
|
sanitize_output: bool = True,
|
|
253
300
|
) -> BaseOpenAPISchema:
|
|
254
301
|
"""Load Open API schema from a Python dictionary.
|
|
@@ -266,7 +313,7 @@ def from_dict(
|
|
|
266
313
|
elif _is_fast_api(app):
|
|
267
314
|
fixups.fast_api.adjust_schema(raw_schema)
|
|
268
315
|
dispatch("before_load_schema", hook_context, raw_schema)
|
|
269
|
-
rate_limiter:
|
|
316
|
+
rate_limiter: Limiter | None = None
|
|
270
317
|
if rate_limit is not None:
|
|
271
318
|
rate_limiter = build_limiter(rate_limit)
|
|
272
319
|
|
|
@@ -356,7 +403,7 @@ NON_STRING_OBJECT_KEY_MESSAGE = (
|
|
|
356
403
|
)
|
|
357
404
|
|
|
358
405
|
|
|
359
|
-
def _format_status_codes(status_codes:
|
|
406
|
+
def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str:
|
|
360
407
|
buffer = io.StringIO()
|
|
361
408
|
for status_code, path in status_codes:
|
|
362
409
|
buffer.write(f" - {status_code} at schema['paths']")
|
|
@@ -367,7 +414,7 @@ def _format_status_codes(status_codes: List[Tuple[int, List[Union[str, int]]]])
|
|
|
367
414
|
|
|
368
415
|
|
|
369
416
|
def _maybe_validate_schema(
|
|
370
|
-
instance:
|
|
417
|
+
instance: dict[str, Any], validator: jsonschema.validators.Draft4Validator, validate_schema: bool
|
|
371
418
|
) -> None:
|
|
372
419
|
from jsonschema import ValidationError
|
|
373
420
|
|
|
@@ -397,16 +444,17 @@ def from_pytest_fixture(
|
|
|
397
444
|
fixture_name: str,
|
|
398
445
|
*,
|
|
399
446
|
app: Any = NOT_SET,
|
|
400
|
-
base_url:
|
|
401
|
-
method:
|
|
402
|
-
endpoint:
|
|
403
|
-
tag:
|
|
404
|
-
operation_id:
|
|
447
|
+
base_url: str | None | NotSet = NOT_SET,
|
|
448
|
+
method: Filter | None = NOT_SET,
|
|
449
|
+
endpoint: Filter | None = NOT_SET,
|
|
450
|
+
tag: Filter | None = NOT_SET,
|
|
451
|
+
operation_id: Filter | None = NOT_SET,
|
|
405
452
|
skip_deprecated_operations: bool = False,
|
|
406
453
|
validate_schema: bool = False,
|
|
407
|
-
data_generation_methods:
|
|
454
|
+
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
455
|
+
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
408
456
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
409
|
-
rate_limit:
|
|
457
|
+
rate_limit: str | None = None,
|
|
410
458
|
sanitize_output: bool = True,
|
|
411
459
|
) -> LazySchema:
|
|
412
460
|
"""Load schema from a ``pytest`` fixture.
|
|
@@ -421,13 +469,13 @@ def from_pytest_fixture(
|
|
|
421
469
|
from ...lazy import LazySchema
|
|
422
470
|
|
|
423
471
|
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
|
424
|
-
_data_generation_methods:
|
|
472
|
+
_data_generation_methods: DataGenerationMethodInput | NotSet
|
|
425
473
|
if data_generation_methods is not NOT_SET:
|
|
426
474
|
data_generation_methods = cast(DataGenerationMethodInput, data_generation_methods)
|
|
427
475
|
_data_generation_methods = DataGenerationMethod.ensure_list(data_generation_methods)
|
|
428
476
|
else:
|
|
429
477
|
_data_generation_methods = data_generation_methods
|
|
430
|
-
rate_limiter:
|
|
478
|
+
rate_limiter: Limiter | None = None
|
|
431
479
|
if rate_limit is not None:
|
|
432
480
|
rate_limiter = build_limiter(rate_limit)
|
|
433
481
|
return LazySchema(
|
|
@@ -441,6 +489,7 @@ def from_pytest_fixture(
|
|
|
441
489
|
skip_deprecated_operations=skip_deprecated_operations,
|
|
442
490
|
validate_schema=validate_schema,
|
|
443
491
|
data_generation_methods=_data_generation_methods,
|
|
492
|
+
generation_config=generation_config,
|
|
444
493
|
code_sample_style=_code_sample_style,
|
|
445
494
|
rate_limiter=rate_limiter,
|
|
446
495
|
sanitize_output=sanitize_output,
|
|
@@ -451,17 +500,18 @@ def from_wsgi(
|
|
|
451
500
|
schema_path: str,
|
|
452
501
|
app: Any,
|
|
453
502
|
*,
|
|
454
|
-
base_url:
|
|
455
|
-
method:
|
|
456
|
-
endpoint:
|
|
457
|
-
tag:
|
|
458
|
-
operation_id:
|
|
503
|
+
base_url: str | None = None,
|
|
504
|
+
method: Filter | None = None,
|
|
505
|
+
endpoint: Filter | None = None,
|
|
506
|
+
tag: Filter | None = None,
|
|
507
|
+
operation_id: Filter | None = None,
|
|
459
508
|
skip_deprecated_operations: bool = False,
|
|
460
509
|
validate_schema: bool = False,
|
|
461
|
-
force_schema_version:
|
|
510
|
+
force_schema_version: str | None = None,
|
|
462
511
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
512
|
+
generation_config: GenerationConfig | None = None,
|
|
463
513
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
464
|
-
rate_limit:
|
|
514
|
+
rate_limit: str | None = None,
|
|
465
515
|
sanitize_output: bool = True,
|
|
466
516
|
**kwargs: Any,
|
|
467
517
|
) -> BaseOpenAPISchema:
|
|
@@ -489,6 +539,7 @@ def from_wsgi(
|
|
|
489
539
|
validate_schema=validate_schema,
|
|
490
540
|
force_schema_version=force_schema_version,
|
|
491
541
|
data_generation_methods=data_generation_methods,
|
|
542
|
+
generation_config=generation_config,
|
|
492
543
|
code_sample_style=code_sample_style,
|
|
493
544
|
location=schema_path,
|
|
494
545
|
rate_limit=rate_limit,
|
|
@@ -511,17 +562,18 @@ def from_aiohttp(
|
|
|
511
562
|
schema_path: str,
|
|
512
563
|
app: Any,
|
|
513
564
|
*,
|
|
514
|
-
base_url:
|
|
515
|
-
method:
|
|
516
|
-
endpoint:
|
|
517
|
-
tag:
|
|
518
|
-
operation_id:
|
|
565
|
+
base_url: str | None = None,
|
|
566
|
+
method: Filter | None = None,
|
|
567
|
+
endpoint: Filter | None = None,
|
|
568
|
+
tag: Filter | None = None,
|
|
569
|
+
operation_id: Filter | None = None,
|
|
519
570
|
skip_deprecated_operations: bool = False,
|
|
520
571
|
validate_schema: bool = False,
|
|
521
|
-
force_schema_version:
|
|
572
|
+
force_schema_version: str | None = None,
|
|
522
573
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
574
|
+
generation_config: GenerationConfig | None = None,
|
|
523
575
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
524
|
-
rate_limit:
|
|
576
|
+
rate_limit: str | None = None,
|
|
525
577
|
sanitize_output: bool = True,
|
|
526
578
|
**kwargs: Any,
|
|
527
579
|
) -> BaseOpenAPISchema:
|
|
@@ -546,6 +598,7 @@ def from_aiohttp(
|
|
|
546
598
|
validate_schema=validate_schema,
|
|
547
599
|
force_schema_version=force_schema_version,
|
|
548
600
|
data_generation_methods=data_generation_methods,
|
|
601
|
+
generation_config=generation_config,
|
|
549
602
|
code_sample_style=code_sample_style,
|
|
550
603
|
rate_limit=rate_limit,
|
|
551
604
|
sanitize_output=sanitize_output,
|
|
@@ -557,17 +610,18 @@ def from_asgi(
|
|
|
557
610
|
schema_path: str,
|
|
558
611
|
app: Any,
|
|
559
612
|
*,
|
|
560
|
-
base_url:
|
|
561
|
-
method:
|
|
562
|
-
endpoint:
|
|
563
|
-
tag:
|
|
564
|
-
operation_id:
|
|
613
|
+
base_url: str | None = None,
|
|
614
|
+
method: Filter | None = None,
|
|
615
|
+
endpoint: Filter | None = None,
|
|
616
|
+
tag: Filter | None = None,
|
|
617
|
+
operation_id: Filter | None = None,
|
|
565
618
|
skip_deprecated_operations: bool = False,
|
|
566
619
|
validate_schema: bool = False,
|
|
567
|
-
force_schema_version:
|
|
620
|
+
force_schema_version: str | None = None,
|
|
568
621
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
622
|
+
generation_config: GenerationConfig | None = None,
|
|
569
623
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
570
|
-
rate_limit:
|
|
624
|
+
rate_limit: str | None = None,
|
|
571
625
|
sanitize_output: bool = True,
|
|
572
626
|
**kwargs: Any,
|
|
573
627
|
) -> BaseOpenAPISchema:
|
|
@@ -594,6 +648,7 @@ def from_asgi(
|
|
|
594
648
|
validate_schema=validate_schema,
|
|
595
649
|
force_schema_version=force_schema_version,
|
|
596
650
|
data_generation_methods=data_generation_methods,
|
|
651
|
+
generation_config=generation_config,
|
|
597
652
|
code_sample_style=code_sample_style,
|
|
598
653
|
location=schema_path,
|
|
599
654
|
rate_limit=rate_limit,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from functools import lru_cache
|
|
3
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
4
5
|
from urllib.parse import urlencode
|
|
5
6
|
|
|
6
7
|
import jsonschema
|
|
@@ -10,6 +11,7 @@ from hypothesis_jsonschema import from_schema
|
|
|
10
11
|
from ..constants import ALL_KEYWORDS
|
|
11
12
|
from .mutations import MutationContext
|
|
12
13
|
from .types import Draw, Schema
|
|
14
|
+
from ....generation import GenerationConfig
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
@@ -27,15 +29,15 @@ class CacheKey:
|
|
|
27
29
|
return hash((self.operation_name, self.location))
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
@lru_cache
|
|
32
|
+
@lru_cache
|
|
31
33
|
def get_validator(cache_key: CacheKey) -> jsonschema.Draft4Validator:
|
|
32
34
|
"""Get JSON Schema validator for the given schema."""
|
|
33
35
|
# Each operation / location combo has only a single schema, therefore could be cached
|
|
34
36
|
return jsonschema.Draft4Validator(cache_key.schema)
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
@lru_cache
|
|
38
|
-
def split_schema(cache_key: CacheKey) ->
|
|
39
|
+
@lru_cache
|
|
40
|
+
def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
|
|
39
41
|
"""Split the schema in two parts.
|
|
40
42
|
|
|
41
43
|
The first one contains only validation JSON Schema keywords, the second one everything else.
|
|
@@ -53,9 +55,10 @@ def negative_schema(
|
|
|
53
55
|
schema: Schema,
|
|
54
56
|
operation_name: str,
|
|
55
57
|
location: str,
|
|
56
|
-
media_type:
|
|
58
|
+
media_type: str | None,
|
|
59
|
+
generation_config: GenerationConfig,
|
|
57
60
|
*,
|
|
58
|
-
custom_formats:
|
|
61
|
+
custom_formats: dict[str, st.SearchStrategy[str]],
|
|
59
62
|
) -> st.SearchStrategy:
|
|
60
63
|
"""A strategy for instances that DO NOT match the input schema.
|
|
61
64
|
|
|
@@ -69,20 +72,22 @@ def negative_schema(
|
|
|
69
72
|
|
|
70
73
|
if location == "query":
|
|
71
74
|
|
|
72
|
-
def filter_values(value:
|
|
75
|
+
def filter_values(value: dict[str, Any]) -> bool:
|
|
73
76
|
return is_non_empty_query(value) and not validator.is_valid(value)
|
|
74
77
|
|
|
75
78
|
else:
|
|
76
79
|
|
|
77
|
-
def filter_values(value:
|
|
80
|
+
def filter_values(value: dict[str, Any]) -> bool:
|
|
78
81
|
return not validator.is_valid(value)
|
|
79
82
|
|
|
80
83
|
return mutated(keywords, non_keywords, location, media_type).flatmap(
|
|
81
|
-
lambda s: from_schema(
|
|
84
|
+
lambda s: from_schema(
|
|
85
|
+
s, custom_formats=custom_formats, allow_x00=generation_config.allow_x00, codec=generation_config.codec
|
|
86
|
+
).filter(filter_values)
|
|
82
87
|
)
|
|
83
88
|
|
|
84
89
|
|
|
85
|
-
def is_non_empty_query(query:
|
|
90
|
+
def is_non_empty_query(query: dict[str, Any]) -> bool:
|
|
86
91
|
# Whether this query parameters will be encoded to a non-empty query string
|
|
87
92
|
result = []
|
|
88
93
|
for key, values in query.items():
|
|
@@ -100,7 +105,7 @@ def is_non_empty_query(query: Dict[str, Any]) -> bool:
|
|
|
100
105
|
|
|
101
106
|
|
|
102
107
|
@st.composite # type: ignore
|
|
103
|
-
def mutated(draw: Draw, keywords: Schema, non_keywords: Schema, location: str, media_type:
|
|
108
|
+
def mutated(draw: Draw, keywords: Schema, non_keywords: Schema, location: str, media_type: str | None) -> Any:
|
|
104
109
|
return MutationContext(
|
|
105
110
|
keywords=keywords, non_keywords=non_keywords, location=location, media_type=media_type
|
|
106
111
|
).mutate(draw)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Schema mutations."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import enum
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from functools import wraps
|
|
5
|
-
from typing import Any, Callable,
|
|
6
|
+
from typing import Any, Callable, Sequence, TypeVar
|
|
6
7
|
|
|
7
8
|
from hypothesis import reject
|
|
8
9
|
from hypothesis import strategies as st
|
|
@@ -28,10 +29,10 @@ class MutationResult(enum.Enum):
|
|
|
28
29
|
SUCCESS = 1
|
|
29
30
|
FAILURE = 2
|
|
30
31
|
|
|
31
|
-
def __ior__(self, other: Any) ->
|
|
32
|
+
def __ior__(self, other: Any) -> MutationResult:
|
|
32
33
|
return self | other
|
|
33
34
|
|
|
34
|
-
def __or__(self, other: Any) ->
|
|
35
|
+
def __or__(self, other: Any) -> MutationResult:
|
|
35
36
|
# Syntactic sugar to simplify handling of multiple results
|
|
36
37
|
if self == MutationResult.SUCCESS:
|
|
37
38
|
return self
|
|
@@ -68,7 +69,7 @@ class MutationContext:
|
|
|
68
69
|
# Schema location within API operation (header, query, etc)
|
|
69
70
|
location: str
|
|
70
71
|
# Payload media type, if available
|
|
71
|
-
media_type:
|
|
72
|
+
media_type: str | None
|
|
72
73
|
|
|
73
74
|
@property
|
|
74
75
|
def is_header_location(self) -> bool:
|
|
@@ -81,7 +82,7 @@ class MutationContext:
|
|
|
81
82
|
def mutate(self, draw: Draw) -> Schema:
|
|
82
83
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
83
84
|
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
84
|
-
mutations:
|
|
85
|
+
mutations: list[Mutation]
|
|
85
86
|
if self.location in ("header", "cookie", "query"):
|
|
86
87
|
# These objects follow this pattern:
|
|
87
88
|
# {
|
|
@@ -225,7 +226,7 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
225
226
|
return MutationResult.SUCCESS
|
|
226
227
|
|
|
227
228
|
|
|
228
|
-
def _get_type_candidates(context: MutationContext, schema: Schema) ->
|
|
229
|
+
def _get_type_candidates(context: MutationContext, schema: Schema) -> set[str]:
|
|
229
230
|
types = set(get_type(schema))
|
|
230
231
|
if context.is_path_location:
|
|
231
232
|
candidates = {"string", "integer", "number", "boolean", "null"} - types
|
|
@@ -334,7 +335,7 @@ def _change_items_object(context: MutationContext, draw: Draw, schema: Schema, i
|
|
|
334
335
|
return MutationResult.SUCCESS
|
|
335
336
|
|
|
336
337
|
|
|
337
|
-
def _change_items_array(context: MutationContext, draw: Draw, schema: Schema, items:
|
|
338
|
+
def _change_items_array(context: MutationContext, draw: Draw, schema: Schema, items: list) -> MutationResult:
|
|
338
339
|
latest_success_index = None
|
|
339
340
|
for idx, item in enumerate(items):
|
|
340
341
|
result = MutationResult.FAILURE
|
|
@@ -397,12 +398,12 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
|
|
|
397
398
|
DEPENDENCIES = {"exclusiveMaximum": "maximum", "exclusiveMinimum": "minimum"}
|
|
398
399
|
|
|
399
400
|
|
|
400
|
-
def get_mutations(draw: Draw, schema: Schema) ->
|
|
401
|
+
def get_mutations(draw: Draw, schema: Schema) -> tuple[Mutation, ...]:
|
|
401
402
|
"""Get mutations possible for a schema."""
|
|
402
403
|
types = get_type(schema)
|
|
403
404
|
# On the top-level of Open API schemas, types are always strings, but inside "schema" objects, they are the same as
|
|
404
405
|
# in JSON Schema, where it could be either a string or an array of strings.
|
|
405
|
-
options:
|
|
406
|
+
options: list[Mutation] = [negate_constraints, change_type]
|
|
406
407
|
if "object" in types:
|
|
407
408
|
options.extend([change_properties, remove_required_property])
|
|
408
409
|
elif "array" in types:
|
|
@@ -414,7 +415,7 @@ def ident(x: T) -> T:
|
|
|
414
415
|
return x
|
|
415
416
|
|
|
416
417
|
|
|
417
|
-
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[
|
|
418
|
+
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[list[T]]:
|
|
418
419
|
"""Returns a strategy that generates randomly ordered lists of T.
|
|
419
420
|
|
|
420
421
|
NOTE. Items should be unique.
|