schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import io
|
|
3
4
|
import json
|
|
4
5
|
import pathlib
|
|
5
6
|
import re
|
|
6
|
-
from typing import IO, Any, Callable, cast
|
|
7
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, cast
|
|
7
8
|
from urllib.parse import urljoin
|
|
8
9
|
|
|
9
10
|
from ... import experimental, fixups
|
|
10
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
|
|
11
15
|
from ...generation import (
|
|
12
16
|
DEFAULT_DATA_GENERATION_METHODS,
|
|
13
|
-
DataGenerationMethodInput,
|
|
14
17
|
DataGenerationMethod,
|
|
18
|
+
DataGenerationMethodInput,
|
|
15
19
|
GenerationConfig,
|
|
16
20
|
)
|
|
17
|
-
from ...constants import WAIT_FOR_SCHEMA_INTERVAL
|
|
18
|
-
from ...exceptions import SchemaError, SchemaErrorType
|
|
19
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
|
|
20
25
|
from ...loaders import load_schema_from_url, load_yaml
|
|
21
26
|
from ...throttling import build_limiter
|
|
22
|
-
from ...types import Filter, NotSet, PathLike
|
|
23
27
|
from ...transports.content_types import is_json_media_type, is_yaml_media_type
|
|
24
28
|
from ...transports.headers import setup_default_headers
|
|
25
|
-
from ...
|
|
26
|
-
from ...constants import NOT_SET
|
|
29
|
+
from ...types import Filter, NotSet, PathLike, Specification
|
|
27
30
|
from . import definitions, validation
|
|
28
31
|
|
|
29
32
|
if TYPE_CHECKING:
|
|
30
|
-
from .schemas import BaseOpenAPISchema
|
|
31
|
-
from ...transports.responses import GenericResponse
|
|
32
33
|
import jsonschema
|
|
33
34
|
from pyrate_limiter import Limiter
|
|
35
|
+
|
|
34
36
|
from ...lazy import LazySchema
|
|
37
|
+
from ...transports.responses import GenericResponse
|
|
38
|
+
from .schemas import BaseOpenAPISchema
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def _is_json_response(response: GenericResponse) -> bool:
|
|
@@ -73,11 +77,12 @@ def from_path(
|
|
|
73
77
|
endpoint: Filter | None = None,
|
|
74
78
|
tag: Filter | None = None,
|
|
75
79
|
operation_id: Filter | None = None,
|
|
76
|
-
skip_deprecated_operations: bool =
|
|
80
|
+
skip_deprecated_operations: bool | None = None,
|
|
77
81
|
validate_schema: bool = False,
|
|
78
82
|
force_schema_version: str | None = None,
|
|
79
83
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
80
84
|
generation_config: GenerationConfig | None = None,
|
|
85
|
+
output_config: OutputConfig | None = None,
|
|
81
86
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
82
87
|
rate_limit: str | None = None,
|
|
83
88
|
encoding: str = "utf8",
|
|
@@ -102,6 +107,7 @@ def from_path(
|
|
|
102
107
|
force_schema_version=force_schema_version,
|
|
103
108
|
data_generation_methods=data_generation_methods,
|
|
104
109
|
generation_config=generation_config,
|
|
110
|
+
output_config=output_config,
|
|
105
111
|
code_sample_style=code_sample_style,
|
|
106
112
|
location=pathlib.Path(path).absolute().as_uri(),
|
|
107
113
|
rate_limit=rate_limit,
|
|
@@ -121,11 +127,12 @@ def from_uri(
|
|
|
121
127
|
endpoint: Filter | None = None,
|
|
122
128
|
tag: Filter | None = None,
|
|
123
129
|
operation_id: Filter | None = None,
|
|
124
|
-
skip_deprecated_operations: bool =
|
|
130
|
+
skip_deprecated_operations: bool | None = None,
|
|
125
131
|
validate_schema: bool = False,
|
|
126
132
|
force_schema_version: str | None = None,
|
|
127
133
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
128
134
|
generation_config: GenerationConfig | None = None,
|
|
135
|
+
output_config: OutputConfig | None = None,
|
|
129
136
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
130
137
|
wait_for_schema: float | None = None,
|
|
131
138
|
rate_limit: str | None = None,
|
|
@@ -156,11 +163,12 @@ def from_uri(
|
|
|
156
163
|
interval=WAIT_FOR_SCHEMA_INTERVAL,
|
|
157
164
|
)
|
|
158
165
|
def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
|
|
159
|
-
return requests.get(_uri, **
|
|
166
|
+
return requests.get(_uri, **_kwargs)
|
|
160
167
|
|
|
161
168
|
else:
|
|
162
169
|
_load_schema = requests.get
|
|
163
170
|
|
|
171
|
+
kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
|
164
172
|
response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
|
|
165
173
|
return from_file(
|
|
166
174
|
response.text,
|
|
@@ -175,6 +183,7 @@ def from_uri(
|
|
|
175
183
|
force_schema_version=force_schema_version,
|
|
176
184
|
data_generation_methods=data_generation_methods,
|
|
177
185
|
generation_config=generation_config,
|
|
186
|
+
output_config=output_config,
|
|
178
187
|
code_sample_style=code_sample_style,
|
|
179
188
|
location=uri,
|
|
180
189
|
rate_limit=rate_limit,
|
|
@@ -215,11 +224,12 @@ def from_file(
|
|
|
215
224
|
endpoint: Filter | None = None,
|
|
216
225
|
tag: Filter | None = None,
|
|
217
226
|
operation_id: Filter | None = None,
|
|
218
|
-
skip_deprecated_operations: bool =
|
|
227
|
+
skip_deprecated_operations: bool | None = None,
|
|
219
228
|
validate_schema: bool = False,
|
|
220
229
|
force_schema_version: str | None = None,
|
|
221
230
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
222
231
|
generation_config: GenerationConfig | None = None,
|
|
232
|
+
output_config: OutputConfig | None = None,
|
|
223
233
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
224
234
|
location: str | None = None,
|
|
225
235
|
rate_limit: str | None = None,
|
|
@@ -266,6 +276,7 @@ def from_file(
|
|
|
266
276
|
force_schema_version=force_schema_version,
|
|
267
277
|
data_generation_methods=data_generation_methods,
|
|
268
278
|
generation_config=generation_config,
|
|
279
|
+
output_config=output_config,
|
|
269
280
|
code_sample_style=code_sample_style,
|
|
270
281
|
location=location,
|
|
271
282
|
rate_limit=rate_limit,
|
|
@@ -289,11 +300,12 @@ def from_dict(
|
|
|
289
300
|
endpoint: Filter | None = None,
|
|
290
301
|
tag: Filter | None = None,
|
|
291
302
|
operation_id: Filter | None = None,
|
|
292
|
-
skip_deprecated_operations: bool =
|
|
303
|
+
skip_deprecated_operations: bool | None = None,
|
|
293
304
|
validate_schema: bool = False,
|
|
294
305
|
force_schema_version: str | None = None,
|
|
295
306
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
296
307
|
generation_config: GenerationConfig | None = None,
|
|
308
|
+
output_config: OutputConfig | None = None,
|
|
297
309
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
298
310
|
location: str | None = None,
|
|
299
311
|
rate_limit: str | None = None,
|
|
@@ -303,6 +315,7 @@ def from_dict(
|
|
|
303
315
|
|
|
304
316
|
:param dict raw_schema: A schema to load.
|
|
305
317
|
"""
|
|
318
|
+
from ... import transports
|
|
306
319
|
from .schemas import OpenApi30, SwaggerV20
|
|
307
320
|
|
|
308
321
|
if not isinstance(raw_schema, dict):
|
|
@@ -320,24 +333,36 @@ def from_dict(
|
|
|
320
333
|
if rate_limit is not None:
|
|
321
334
|
rate_limiter = build_limiter(rate_limit)
|
|
322
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
|
+
|
|
323
349
|
def init_openapi_2() -> SwaggerV20:
|
|
324
350
|
_maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
|
|
325
351
|
instance = SwaggerV20(
|
|
326
352
|
raw_schema,
|
|
353
|
+
specification=Specification.OPENAPI,
|
|
327
354
|
app=app,
|
|
328
355
|
base_url=base_url,
|
|
329
|
-
|
|
330
|
-
endpoint=endpoint,
|
|
331
|
-
tag=tag,
|
|
332
|
-
operation_id=operation_id,
|
|
333
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
356
|
+
filter_set=filter_set,
|
|
334
357
|
validate_schema=validate_schema,
|
|
335
358
|
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
|
336
359
|
generation_config=generation_config or GenerationConfig(),
|
|
360
|
+
output_config=output_config or OutputConfig(),
|
|
337
361
|
code_sample_style=_code_sample_style,
|
|
338
362
|
location=location,
|
|
339
363
|
rate_limiter=rate_limiter,
|
|
340
364
|
sanitize_output=sanitize_output,
|
|
365
|
+
transport=transports.get(app),
|
|
341
366
|
)
|
|
342
367
|
dispatch("after_load_schema", hook_context, instance)
|
|
343
368
|
return instance
|
|
@@ -365,20 +390,19 @@ def from_dict(
|
|
|
365
390
|
_maybe_validate_schema(raw_schema, validator, validate_schema)
|
|
366
391
|
instance = OpenApi30(
|
|
367
392
|
raw_schema,
|
|
393
|
+
specification=Specification.OPENAPI,
|
|
368
394
|
app=app,
|
|
369
395
|
base_url=base_url,
|
|
370
|
-
|
|
371
|
-
endpoint=endpoint,
|
|
372
|
-
tag=tag,
|
|
373
|
-
operation_id=operation_id,
|
|
374
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
396
|
+
filter_set=filter_set,
|
|
375
397
|
validate_schema=validate_schema,
|
|
376
398
|
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
|
377
399
|
generation_config=generation_config or GenerationConfig(),
|
|
400
|
+
output_config=output_config or OutputConfig(),
|
|
378
401
|
code_sample_style=_code_sample_style,
|
|
379
402
|
location=location,
|
|
380
403
|
rate_limiter=rate_limiter,
|
|
381
404
|
sanitize_output=sanitize_output,
|
|
405
|
+
transport=transports.get(app),
|
|
382
406
|
)
|
|
383
407
|
dispatch("after_load_schema", hook_context, instance)
|
|
384
408
|
return instance
|
|
@@ -418,7 +442,7 @@ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str
|
|
|
418
442
|
for status_code, path in status_codes:
|
|
419
443
|
buffer.write(f" - {status_code} at schema['paths']")
|
|
420
444
|
for chunk in path:
|
|
421
|
-
buffer.write(f"[{
|
|
445
|
+
buffer.write(f"[{chunk!r}]")
|
|
422
446
|
buffer.write("['responses']\n")
|
|
423
447
|
return buffer.getvalue().rstrip()
|
|
424
448
|
|
|
@@ -459,10 +483,11 @@ def from_pytest_fixture(
|
|
|
459
483
|
endpoint: Filter | None = NOT_SET,
|
|
460
484
|
tag: Filter | None = NOT_SET,
|
|
461
485
|
operation_id: Filter | None = NOT_SET,
|
|
462
|
-
skip_deprecated_operations: bool =
|
|
486
|
+
skip_deprecated_operations: bool | None = None,
|
|
463
487
|
validate_schema: bool = False,
|
|
464
488
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
465
489
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
490
|
+
output_config: OutputConfig | NotSet = NOT_SET,
|
|
466
491
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
467
492
|
rate_limit: str | None = None,
|
|
468
493
|
sanitize_output: bool = True,
|
|
@@ -488,18 +513,27 @@ def from_pytest_fixture(
|
|
|
488
513
|
rate_limiter: Limiter | None = None
|
|
489
514
|
if rate_limit is not None:
|
|
490
515
|
rate_limiter = build_limiter(rate_limit)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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,
|
|
495
522
|
method=method,
|
|
496
523
|
endpoint=endpoint,
|
|
497
524
|
tag=tag,
|
|
498
525
|
operation_id=operation_id,
|
|
499
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,
|
|
500
533
|
validate_schema=validate_schema,
|
|
501
534
|
data_generation_methods=_data_generation_methods,
|
|
502
535
|
generation_config=generation_config,
|
|
536
|
+
output_config=output_config,
|
|
503
537
|
code_sample_style=_code_sample_style,
|
|
504
538
|
rate_limiter=rate_limiter,
|
|
505
539
|
sanitize_output=sanitize_output,
|
|
@@ -515,11 +549,12 @@ def from_wsgi(
|
|
|
515
549
|
endpoint: Filter | None = None,
|
|
516
550
|
tag: Filter | None = None,
|
|
517
551
|
operation_id: Filter | None = None,
|
|
518
|
-
skip_deprecated_operations: bool =
|
|
552
|
+
skip_deprecated_operations: bool | None = None,
|
|
519
553
|
validate_schema: bool = False,
|
|
520
554
|
force_schema_version: str | None = None,
|
|
521
555
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
522
556
|
generation_config: GenerationConfig | None = None,
|
|
557
|
+
output_config: OutputConfig | None = None,
|
|
523
558
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
524
559
|
rate_limit: str | None = None,
|
|
525
560
|
sanitize_output: bool = True,
|
|
@@ -530,9 +565,10 @@ def from_wsgi(
|
|
|
530
565
|
:param str schema_path: An in-app relative URL to the schema.
|
|
531
566
|
:param app: A WSGI app instance.
|
|
532
567
|
"""
|
|
533
|
-
from ...transports.responses import WSGIResponse
|
|
534
568
|
from werkzeug.test import Client
|
|
535
569
|
|
|
570
|
+
from ...transports.responses import WSGIResponse
|
|
571
|
+
|
|
536
572
|
require_relative_url(schema_path)
|
|
537
573
|
setup_default_headers(kwargs)
|
|
538
574
|
client = Client(app, WSGIResponse)
|
|
@@ -550,6 +586,7 @@ def from_wsgi(
|
|
|
550
586
|
force_schema_version=force_schema_version,
|
|
551
587
|
data_generation_methods=data_generation_methods,
|
|
552
588
|
generation_config=generation_config,
|
|
589
|
+
output_config=output_config,
|
|
553
590
|
code_sample_style=code_sample_style,
|
|
554
591
|
location=schema_path,
|
|
555
592
|
rate_limit=rate_limit,
|
|
@@ -559,9 +596,9 @@ def from_wsgi(
|
|
|
559
596
|
|
|
560
597
|
|
|
561
598
|
def get_loader_for_app(app: Any) -> Callable:
|
|
562
|
-
from
|
|
599
|
+
from ...transports.asgi import is_asgi_app
|
|
563
600
|
|
|
564
|
-
if
|
|
601
|
+
if is_asgi_app(app):
|
|
565
602
|
return from_asgi
|
|
566
603
|
if app.__class__.__module__.startswith("aiohttp."):
|
|
567
604
|
return from_aiohttp
|
|
@@ -577,11 +614,12 @@ def from_aiohttp(
|
|
|
577
614
|
endpoint: Filter | None = None,
|
|
578
615
|
tag: Filter | None = None,
|
|
579
616
|
operation_id: Filter | None = None,
|
|
580
|
-
skip_deprecated_operations: bool =
|
|
617
|
+
skip_deprecated_operations: bool | None = None,
|
|
581
618
|
validate_schema: bool = False,
|
|
582
619
|
force_schema_version: str | None = None,
|
|
583
620
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
584
621
|
generation_config: GenerationConfig | None = None,
|
|
622
|
+
output_config: OutputConfig | None = None,
|
|
585
623
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
586
624
|
rate_limit: str | None = None,
|
|
587
625
|
sanitize_output: bool = True,
|
|
@@ -609,6 +647,7 @@ def from_aiohttp(
|
|
|
609
647
|
force_schema_version=force_schema_version,
|
|
610
648
|
data_generation_methods=data_generation_methods,
|
|
611
649
|
generation_config=generation_config,
|
|
650
|
+
output_config=output_config,
|
|
612
651
|
code_sample_style=code_sample_style,
|
|
613
652
|
rate_limit=rate_limit,
|
|
614
653
|
sanitize_output=sanitize_output,
|
|
@@ -625,11 +664,12 @@ def from_asgi(
|
|
|
625
664
|
endpoint: Filter | None = None,
|
|
626
665
|
tag: Filter | None = None,
|
|
627
666
|
operation_id: Filter | None = None,
|
|
628
|
-
skip_deprecated_operations: bool =
|
|
667
|
+
skip_deprecated_operations: bool | None = None,
|
|
629
668
|
validate_schema: bool = False,
|
|
630
669
|
force_schema_version: str | None = None,
|
|
631
670
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
632
671
|
generation_config: GenerationConfig | None = None,
|
|
672
|
+
output_config: OutputConfig | None = None,
|
|
633
673
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
634
674
|
rate_limit: str | None = None,
|
|
635
675
|
sanitize_output: bool = True,
|
|
@@ -659,6 +699,7 @@ def from_asgi(
|
|
|
659
699
|
force_schema_version=force_schema_version,
|
|
660
700
|
data_generation_methods=data_generation_methods,
|
|
661
701
|
generation_config=generation_config,
|
|
702
|
+
output_config=output_config,
|
|
662
703
|
code_sample_style=code_sample_style,
|
|
663
704
|
location=schema_path,
|
|
664
705
|
rate_limit=rate_limit,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Collection
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from hypothesis import strategies as st
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MEDIA_TYPES: dict[str, st.SearchStrategy[bytes]] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliases: Collection[str] = ()) -> None:
|
|
13
|
+
"""Register a strategy for the given media type."""
|
|
14
|
+
from ...serializers import SerializerContext, register
|
|
15
|
+
|
|
16
|
+
@register(name, aliases=aliases)
|
|
17
|
+
class MediaTypeSerializer:
|
|
18
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
19
|
+
return {"data": value}
|
|
20
|
+
|
|
21
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
22
|
+
return {"data": value}
|
|
23
|
+
|
|
24
|
+
MEDIA_TYPES[name] = strategy
|
|
25
|
+
for alias in aliases:
|
|
26
|
+
MEDIA_TYPES[alias] = strategy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unregister_all() -> None:
|
|
30
|
+
from ...serializers import unregister
|
|
31
|
+
|
|
32
|
+
for media_type in MEDIA_TYPES:
|
|
33
|
+
unregister(media_type)
|
|
34
|
+
MEDIA_TYPES.clear()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from functools import lru_cache
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
5
6
|
from urllib.parse import urlencode
|
|
6
7
|
|
|
7
8
|
import jsonschema
|
|
@@ -10,8 +11,10 @@ from hypothesis_jsonschema import from_schema
|
|
|
10
11
|
|
|
11
12
|
from ..constants import ALL_KEYWORDS
|
|
12
13
|
from .mutations import MutationContext
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ....generation import GenerationConfig
|
|
17
|
+
from .types import Draw, Schema
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
@dataclass
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Schema mutations."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import enum
|
|
4
6
|
from dataclasses import dataclass
|
|
5
7
|
from functools import wraps
|
|
@@ -79,6 +81,10 @@ class MutationContext:
|
|
|
79
81
|
def is_path_location(self) -> bool:
|
|
80
82
|
return self.location == "path"
|
|
81
83
|
|
|
84
|
+
@property
|
|
85
|
+
def is_query_location(self) -> bool:
|
|
86
|
+
return self.location == "query"
|
|
87
|
+
|
|
82
88
|
def mutate(self, draw: Draw) -> Schema:
|
|
83
89
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
84
90
|
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
@@ -173,7 +179,7 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
173
179
|
else:
|
|
174
180
|
candidate = draw(st.sampled_from(sorted(required)))
|
|
175
181
|
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties")) # type: ignore
|
|
176
|
-
candidates = [candidate
|
|
182
|
+
candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
|
|
177
183
|
property_name = draw(st.sampled_from(candidates))
|
|
178
184
|
required.remove(property_name)
|
|
179
185
|
if not required:
|
|
@@ -201,8 +207,11 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
201
207
|
if context.media_type == "application/x-www-form-urlencoded":
|
|
202
208
|
# Form data should be an object, do not change it
|
|
203
209
|
return MutationResult.FAILURE
|
|
204
|
-
#
|
|
205
|
-
|
|
210
|
+
# For headers, query and path parameters, if the current type is string, then it already
|
|
211
|
+
# includes all possible values as those parameters will be stringified before sending,
|
|
212
|
+
# therefore it can't be negated.
|
|
213
|
+
types = get_type(schema)
|
|
214
|
+
if "string" in types and (context.is_header_location or context.is_path_location or context.is_query_location):
|
|
206
215
|
return MutationResult.FAILURE
|
|
207
216
|
candidates = _get_type_candidates(context, schema)
|
|
208
217
|
if not candidates:
|
|
@@ -217,9 +226,10 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
217
226
|
candidate = draw(st.sampled_from(sorted(candidates)))
|
|
218
227
|
candidates.remove(candidate)
|
|
219
228
|
enabled_types = draw(st.shared(FeatureStrategy(), key="types")) # type: ignore
|
|
220
|
-
remaining_candidates = [
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
remaining_candidates = [
|
|
230
|
+
candidate,
|
|
231
|
+
*sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
|
|
232
|
+
]
|
|
223
233
|
new_type = draw(st.sampled_from(remaining_candidates))
|
|
224
234
|
schema["type"] = new_type
|
|
225
235
|
prevent_unsatisfiable_schema(schema, new_type)
|
|
@@ -362,6 +372,11 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
|
|
|
362
372
|
# Should we negate this key?
|
|
363
373
|
if k == "required":
|
|
364
374
|
return v != []
|
|
375
|
+
if k in ("example", "examples"):
|
|
376
|
+
return False
|
|
377
|
+
if context.is_path_location and k == "minLength" and v == 1:
|
|
378
|
+
# Empty path parameter will be filtered out
|
|
379
|
+
return False
|
|
365
380
|
return not (
|
|
366
381
|
k in ("type", "properties", "items", "minItems")
|
|
367
382
|
or (k == "additionalProperties" and context.is_header_location)
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, ClassVar, Iterable
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Iterable
|
|
5
6
|
|
|
6
7
|
from ...exceptions import OperationSchemaError
|
|
7
|
-
from ...models import APIOperation
|
|
8
8
|
from ...parameters import Parameter
|
|
9
9
|
from .converter import to_json_schema_recursive
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ...models import APIOperation
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@dataclass(eq=False)
|
|
13
16
|
class OpenAPIParameter(Parameter):
|
|
@@ -18,6 +21,7 @@ class OpenAPIParameter(Parameter):
|
|
|
18
21
|
nullable_field: ClassVar[str]
|
|
19
22
|
supported_jsonschema_keywords: ClassVar[tuple[str, ...]]
|
|
20
23
|
|
|
24
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
21
25
|
@property
|
|
22
26
|
def description(self) -> str | None:
|
|
23
27
|
"""A brief parameter description."""
|
|
@@ -47,16 +51,26 @@ class OpenAPIParameter(Parameter):
|
|
|
47
51
|
|
|
48
52
|
@property
|
|
49
53
|
def is_header(self) -> bool:
|
|
50
|
-
|
|
54
|
+
return self.location in ("header", "cookie")
|
|
51
55
|
|
|
52
|
-
def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
|
|
56
|
+
def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
53
57
|
"""Convert parameter's definition to JSON Schema."""
|
|
58
|
+
# JSON Schema allows `examples` as an array
|
|
59
|
+
examples = []
|
|
60
|
+
if self.examples_field in self.definition:
|
|
61
|
+
examples.extend(
|
|
62
|
+
[example["value"] for example in self.definition[self.examples_field].values() if "value" in example]
|
|
63
|
+
)
|
|
64
|
+
if self.example_field in self.definition:
|
|
65
|
+
examples.append(self.definition[self.example_field])
|
|
54
66
|
schema = self.from_open_api_to_json_schema(operation, self.definition)
|
|
55
|
-
|
|
67
|
+
if examples:
|
|
68
|
+
schema["examples"] = examples
|
|
69
|
+
return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
|
|
56
70
|
|
|
57
|
-
def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
def transform_keywords(self, schema: dict[str, Any], *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
58
72
|
"""Transform Open API specific keywords into JSON Schema compatible form."""
|
|
59
|
-
definition = to_json_schema_recursive(schema, self.nullable_field)
|
|
73
|
+
definition = to_json_schema_recursive(schema, self.nullable_field, update_quantifiers=update_quantifiers)
|
|
60
74
|
# Headers are strings, but it is not always explicitly defined in the schema. By preparing them properly, we
|
|
61
75
|
# can achieve significant performance improvements for such cases.
|
|
62
76
|
# For reference (my machine) - running a single test with 100 examples with the resulting strategy:
|
|
@@ -116,12 +130,10 @@ class OpenAPI20Parameter(OpenAPIParameter):
|
|
|
116
130
|
"uniqueItems",
|
|
117
131
|
"enum",
|
|
118
132
|
"multipleOf",
|
|
133
|
+
"example",
|
|
134
|
+
"examples",
|
|
119
135
|
)
|
|
120
136
|
|
|
121
|
-
@property
|
|
122
|
-
def is_header(self) -> bool:
|
|
123
|
-
return self.location == "header"
|
|
124
|
-
|
|
125
137
|
|
|
126
138
|
@dataclass(eq=False)
|
|
127
139
|
class OpenAPI30Parameter(OpenAPIParameter):
|
|
@@ -162,12 +174,10 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
|
162
174
|
"properties",
|
|
163
175
|
"additionalProperties",
|
|
164
176
|
"format",
|
|
177
|
+
"example",
|
|
178
|
+
"examples",
|
|
165
179
|
)
|
|
166
180
|
|
|
167
|
-
@property
|
|
168
|
-
def is_header(self) -> bool:
|
|
169
|
-
return self.location in ("header", "cookie")
|
|
170
|
-
|
|
171
181
|
def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
|
|
172
182
|
open_api_schema = get_parameter_schema(operation, open_api_schema)
|
|
173
183
|
return super().from_open_api_to_json_schema(operation, open_api_schema)
|
|
@@ -216,15 +226,17 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
|
|
|
216
226
|
"allOf",
|
|
217
227
|
"properties",
|
|
218
228
|
"additionalProperties",
|
|
229
|
+
"example",
|
|
230
|
+
"examples",
|
|
219
231
|
)
|
|
220
232
|
# NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
|
|
221
233
|
# the precedence rules consistent.
|
|
222
234
|
|
|
223
|
-
def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
|
|
235
|
+
def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
224
236
|
"""Convert body definition to JSON Schema."""
|
|
225
237
|
# `schema` is required in Open API 2.0 when the `in` keyword is `body`
|
|
226
238
|
schema = self.definition["schema"]
|
|
227
|
-
return self.transform_keywords(schema)
|
|
239
|
+
return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
|
|
228
240
|
|
|
229
241
|
|
|
230
242
|
FORM_MEDIA_TYPES = ("multipart/form-data", "application/x-www-form-urlencoded")
|
|
@@ -243,13 +255,13 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
|
|
|
243
255
|
required: bool = False
|
|
244
256
|
description: str | None = None
|
|
245
257
|
|
|
246
|
-
def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
|
|
258
|
+
def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
247
259
|
"""Convert body definition to JSON Schema."""
|
|
248
260
|
schema = get_media_type_schema(self.definition)
|
|
249
|
-
return self.transform_keywords(schema)
|
|
261
|
+
return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
|
|
250
262
|
|
|
251
|
-
def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
252
|
-
definition = super().transform_keywords(schema)
|
|
263
|
+
def transform_keywords(self, schema: dict[str, Any], *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
264
|
+
definition = super().transform_keywords(schema, update_quantifiers=update_quantifiers)
|
|
253
265
|
if self.is_form:
|
|
254
266
|
# It significantly reduces the "filtering" part of data generation.
|
|
255
267
|
definition.setdefault("type", "object")
|
|
@@ -287,12 +299,14 @@ class OpenAPI20CompositeBody(OpenAPIBody, OpenAPI20Parameter):
|
|
|
287
299
|
# We generate an object for formData - it is always required.
|
|
288
300
|
return bool(self.definition)
|
|
289
301
|
|
|
290
|
-
def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
|
|
302
|
+
def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
|
|
291
303
|
"""The composite body is transformed into an "object" JSON Schema."""
|
|
292
|
-
return parameters_to_json_schema(operation, self.definition)
|
|
304
|
+
return parameters_to_json_schema(operation, self.definition, update_quantifiers=update_quantifiers)
|
|
293
305
|
|
|
294
306
|
|
|
295
|
-
def parameters_to_json_schema(
|
|
307
|
+
def parameters_to_json_schema(
|
|
308
|
+
operation: APIOperation, parameters: Iterable[OpenAPIParameter], *, update_quantifiers: bool = True
|
|
309
|
+
) -> dict[str, Any]:
|
|
296
310
|
"""Create an "object" JSON schema from a list of Open API parameters.
|
|
297
311
|
|
|
298
312
|
:param List[OpenAPIParameter] parameters: A list of Open API parameters related to the same location. All of
|
|
@@ -332,7 +346,7 @@ def parameters_to_json_schema(operation: APIOperation, parameters: Iterable[Open
|
|
|
332
346
|
required = []
|
|
333
347
|
for parameter in parameters:
|
|
334
348
|
name = parameter.name
|
|
335
|
-
properties[name] = parameter.as_json_schema(operation)
|
|
349
|
+
properties[name] = parameter.as_json_schema(operation, update_quantifiers=update_quantifiers)
|
|
336
350
|
# If parameter names are duplicated, we need to avoid duplicate entries in `required` anyway
|
|
337
351
|
if parameter.is_required and name not in required:
|
|
338
352
|
required.append(name)
|