schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, NoReturn
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from schemathesis.core.registries import Registry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomHelpMessageChoice(click.Choice):
|
|
12
|
+
"""Allows you to customize how choices are displayed in the help message."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args: Any, choices_repr: str, **kwargs: Any):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
self.choices_repr = choices_repr
|
|
17
|
+
|
|
18
|
+
def get_metavar(self, param: click.Parameter) -> str:
|
|
19
|
+
return self.choices_repr
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseCsvChoice(click.Choice):
|
|
23
|
+
def parse_value(self, value: str) -> tuple[list[str], set[str]]:
|
|
24
|
+
selected = [item.strip() for item in value.split(",") if item.strip()]
|
|
25
|
+
if not self.case_sensitive:
|
|
26
|
+
invalid_options = {
|
|
27
|
+
item for item in selected if item.upper() not in {choice.upper() for choice in self.choices}
|
|
28
|
+
}
|
|
29
|
+
else:
|
|
30
|
+
invalid_options = set(selected) - set(self.choices)
|
|
31
|
+
return selected, invalid_options
|
|
32
|
+
|
|
33
|
+
def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn: # type: ignore[misc]
|
|
34
|
+
# Sort to keep the error output consistent with the passed values
|
|
35
|
+
sorted_options = ", ".join(sorted(invalid_options, key=selected.index))
|
|
36
|
+
available_options = ", ".join(self.choices)
|
|
37
|
+
self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CsvChoice(BaseCsvChoice):
|
|
41
|
+
def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
|
|
42
|
+
selected, invalid_options = self.parse_value(value)
|
|
43
|
+
if not invalid_options and selected:
|
|
44
|
+
return selected
|
|
45
|
+
self.fail_on_invalid_options(invalid_options, selected)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CsvEnumChoice(BaseCsvChoice):
|
|
49
|
+
def __init__(self, choices: type[Enum], case_sensitive: bool = False):
|
|
50
|
+
self.enum = choices
|
|
51
|
+
super().__init__(tuple(el.name.lower() for el in choices), case_sensitive=case_sensitive)
|
|
52
|
+
|
|
53
|
+
def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[Enum]:
|
|
54
|
+
selected, invalid_options = self.parse_value(value)
|
|
55
|
+
if not invalid_options and selected:
|
|
56
|
+
# Match case-insensitively to find the correct enum
|
|
57
|
+
return [
|
|
58
|
+
next(enum_value for enum_value in self.enum if enum_value.value.upper() == item.upper())
|
|
59
|
+
for item in selected
|
|
60
|
+
]
|
|
61
|
+
self.fail_on_invalid_options(invalid_options, selected)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class RegistryChoice(BaseCsvChoice):
|
|
65
|
+
def __init__(self, registry: Registry, with_all: bool = False) -> None:
|
|
66
|
+
self.registry = registry
|
|
67
|
+
self.case_sensitive = True
|
|
68
|
+
self.with_all = with_all
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def choices(self) -> list[str]:
|
|
72
|
+
choices = self.registry.get_all_names()
|
|
73
|
+
if self.with_all:
|
|
74
|
+
choices.append("all")
|
|
75
|
+
return choices
|
|
76
|
+
|
|
77
|
+
def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
|
|
78
|
+
selected, invalid_options = self.parse_value(value)
|
|
79
|
+
if not invalid_options and selected:
|
|
80
|
+
return selected
|
|
81
|
+
self.fail_on_invalid_options(invalid_options, selected)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from random import Random
|
|
8
|
+
|
|
9
|
+
from schemathesis.config._checks import (
|
|
10
|
+
CheckConfig,
|
|
11
|
+
ChecksConfig,
|
|
12
|
+
NotAServerErrorConfig,
|
|
13
|
+
PositiveDataAcceptanceConfig,
|
|
14
|
+
SimpleCheckConfig,
|
|
15
|
+
)
|
|
16
|
+
from schemathesis.config._diff_base import DiffBase
|
|
17
|
+
from schemathesis.config._error import ConfigError
|
|
18
|
+
from schemathesis.config._generation import GenerationConfig
|
|
19
|
+
from schemathesis.config._health_check import HealthCheck
|
|
20
|
+
from schemathesis.config._output import OutputConfig, SanitizationConfig, TruncationConfig
|
|
21
|
+
from schemathesis.config._phases import (
|
|
22
|
+
CoveragePhaseConfig,
|
|
23
|
+
InferenceAlgorithm,
|
|
24
|
+
PhaseConfig,
|
|
25
|
+
PhasesConfig,
|
|
26
|
+
StatefulPhaseConfig,
|
|
27
|
+
)
|
|
28
|
+
from schemathesis.config._projects import ProjectConfig, ProjectsConfig, get_workers_count
|
|
29
|
+
from schemathesis.config._report import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat, ReportsConfig
|
|
30
|
+
from schemathesis.config._warnings import SchemathesisWarning, WarningsConfig
|
|
31
|
+
|
|
32
|
+
if sys.version_info < (3, 11):
|
|
33
|
+
import tomli
|
|
34
|
+
else:
|
|
35
|
+
import tomllib as tomli
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"SchemathesisConfig",
|
|
39
|
+
"ConfigError",
|
|
40
|
+
"HealthCheck",
|
|
41
|
+
"ReportConfig",
|
|
42
|
+
"ReportsConfig",
|
|
43
|
+
"ReportFormat",
|
|
44
|
+
"DEFAULT_REPORT_DIRECTORY",
|
|
45
|
+
"GenerationConfig",
|
|
46
|
+
"OutputConfig",
|
|
47
|
+
"SanitizationConfig",
|
|
48
|
+
"TruncationConfig",
|
|
49
|
+
"ChecksConfig",
|
|
50
|
+
"CheckConfig",
|
|
51
|
+
"NotAServerErrorConfig",
|
|
52
|
+
"PositiveDataAcceptanceConfig",
|
|
53
|
+
"SimpleCheckConfig",
|
|
54
|
+
"PhaseConfig",
|
|
55
|
+
"PhasesConfig",
|
|
56
|
+
"CoveragePhaseConfig",
|
|
57
|
+
"StatefulPhaseConfig",
|
|
58
|
+
"InferenceAlgorithm",
|
|
59
|
+
"ProjectsConfig",
|
|
60
|
+
"ProjectConfig",
|
|
61
|
+
"get_workers_count",
|
|
62
|
+
"SchemathesisWarning",
|
|
63
|
+
"WarningsConfig",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(repr=False)
|
|
68
|
+
class SchemathesisConfig(DiffBase):
|
|
69
|
+
color: bool | None
|
|
70
|
+
suppress_health_check: list[HealthCheck]
|
|
71
|
+
_seed: int | None
|
|
72
|
+
wait_for_schema: float | int | None
|
|
73
|
+
max_failures: int | None
|
|
74
|
+
reports: ReportsConfig
|
|
75
|
+
output: OutputConfig
|
|
76
|
+
projects: ProjectsConfig
|
|
77
|
+
|
|
78
|
+
__slots__ = (
|
|
79
|
+
"color",
|
|
80
|
+
"suppress_health_check",
|
|
81
|
+
"_seed",
|
|
82
|
+
"wait_for_schema",
|
|
83
|
+
"max_failures",
|
|
84
|
+
"reports",
|
|
85
|
+
"output",
|
|
86
|
+
"projects",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
color: bool | None = None,
|
|
93
|
+
suppress_health_check: list[HealthCheck] | None = None,
|
|
94
|
+
seed: int | None = None,
|
|
95
|
+
wait_for_schema: float | int | None = None,
|
|
96
|
+
max_failures: int | None = None,
|
|
97
|
+
reports: ReportsConfig | None = None,
|
|
98
|
+
output: OutputConfig | None = None,
|
|
99
|
+
projects: ProjectsConfig | None = None,
|
|
100
|
+
):
|
|
101
|
+
self.color = color
|
|
102
|
+
self.suppress_health_check = suppress_health_check or []
|
|
103
|
+
self._seed = seed
|
|
104
|
+
self.wait_for_schema = wait_for_schema
|
|
105
|
+
self.max_failures = max_failures
|
|
106
|
+
self.reports = reports or ReportsConfig()
|
|
107
|
+
self.output = output or OutputConfig()
|
|
108
|
+
self.projects = projects or ProjectsConfig()
|
|
109
|
+
self.projects._set_parent(self)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def seed(self) -> int:
|
|
113
|
+
if self._seed is None:
|
|
114
|
+
self._seed = Random().getrandbits(128)
|
|
115
|
+
return self._seed
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def discover(cls) -> SchemathesisConfig:
|
|
119
|
+
"""Discover the Schemathesis configuration file.
|
|
120
|
+
|
|
121
|
+
Search for 'schemathesis.toml' in the current directory and then in each parent directory,
|
|
122
|
+
stopping when a directory containing a '.git' folder is encountered or the filesystem root is reached.
|
|
123
|
+
If a config file is found, load it; otherwise, return a default configuration.
|
|
124
|
+
"""
|
|
125
|
+
current_dir = os.getcwd()
|
|
126
|
+
config_file = None
|
|
127
|
+
|
|
128
|
+
while True:
|
|
129
|
+
candidate = os.path.join(current_dir, "schemathesis.toml")
|
|
130
|
+
if os.path.isfile(candidate):
|
|
131
|
+
config_file = candidate
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
# Stop searching if we've reached a git repository root
|
|
135
|
+
git_dir = os.path.join(current_dir, ".git")
|
|
136
|
+
if os.path.isdir(git_dir):
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
# Stop if we've reached the filesystem root
|
|
140
|
+
parent = os.path.dirname(current_dir)
|
|
141
|
+
if parent == current_dir:
|
|
142
|
+
break
|
|
143
|
+
current_dir = parent
|
|
144
|
+
|
|
145
|
+
if config_file:
|
|
146
|
+
return cls.from_path(config_file)
|
|
147
|
+
return cls()
|
|
148
|
+
|
|
149
|
+
def update(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
color: bool | None = None,
|
|
153
|
+
suppress_health_check: list[HealthCheck] | None = None,
|
|
154
|
+
seed: int | None = None,
|
|
155
|
+
wait_for_schema: float | int | None = None,
|
|
156
|
+
max_failures: int | None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Set top-level configuration options."""
|
|
159
|
+
if color is not None:
|
|
160
|
+
self.color = color
|
|
161
|
+
if suppress_health_check is not None:
|
|
162
|
+
self.suppress_health_check = suppress_health_check
|
|
163
|
+
if seed is not None:
|
|
164
|
+
self._seed = seed
|
|
165
|
+
if wait_for_schema is not None:
|
|
166
|
+
self.wait_for_schema = wait_for_schema
|
|
167
|
+
if max_failures is not None:
|
|
168
|
+
self.max_failures = max_failures
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_path(cls, path: PathLike | str) -> SchemathesisConfig:
|
|
172
|
+
"""Load configuration from a file path."""
|
|
173
|
+
with open(path, encoding="utf-8") as fd:
|
|
174
|
+
return cls.from_str(fd.read())
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_str(cls, data: str) -> SchemathesisConfig:
|
|
178
|
+
"""Parse configuration from a string."""
|
|
179
|
+
parsed = tomli.loads(data)
|
|
180
|
+
return cls.from_dict(parsed)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_dict(cls, data: dict) -> SchemathesisConfig:
|
|
184
|
+
"""Create a config instance from a dictionary."""
|
|
185
|
+
from jsonschema.exceptions import ValidationError
|
|
186
|
+
|
|
187
|
+
from schemathesis.config._validator import CONFIG_VALIDATOR
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
CONFIG_VALIDATOR.validate(data)
|
|
191
|
+
except ValidationError as exc:
|
|
192
|
+
raise ConfigError.from_validation_error(exc) from None
|
|
193
|
+
return cls(
|
|
194
|
+
color=data.get("color"),
|
|
195
|
+
suppress_health_check=[HealthCheck(name) for name in data.get("suppress-health-check", [])],
|
|
196
|
+
seed=data.get("seed"),
|
|
197
|
+
wait_for_schema=data.get("wait-for-schema"),
|
|
198
|
+
max_failures=data.get("max-failures"),
|
|
199
|
+
reports=ReportsConfig.from_dict(data.get("reports", {})),
|
|
200
|
+
output=OutputConfig.from_dict(data.get("output", {})),
|
|
201
|
+
projects=ProjectsConfig.from_dict(data),
|
|
202
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from schemathesis.config._diff_base import DiffBase
|
|
7
|
+
from schemathesis.config._env import resolve
|
|
8
|
+
from schemathesis.config._error import ConfigError
|
|
9
|
+
from schemathesis.core.validation import is_latin_1_encodable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(repr=False)
|
|
13
|
+
class AuthConfig(DiffBase):
|
|
14
|
+
basic: tuple[str, str] | None
|
|
15
|
+
|
|
16
|
+
__slots__ = ("basic",)
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
basic: dict[str, str] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
if basic is not None:
|
|
24
|
+
assert "username" in basic
|
|
25
|
+
username = resolve(basic["username"])
|
|
26
|
+
assert "password" in basic
|
|
27
|
+
password = resolve(basic["password"])
|
|
28
|
+
_validate_basic(username, password)
|
|
29
|
+
self.basic = (username, password)
|
|
30
|
+
else:
|
|
31
|
+
self.basic = None
|
|
32
|
+
|
|
33
|
+
def update(self, *, basic: tuple[str, str] | None = None) -> None:
|
|
34
|
+
if basic is not None:
|
|
35
|
+
_validate_basic(*basic)
|
|
36
|
+
self.basic = basic
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, data: dict[str, Any]) -> AuthConfig:
|
|
40
|
+
return cls(basic=data.get("basic"))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_defined(self) -> bool:
|
|
44
|
+
return self.basic is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_basic(username: str, password: str) -> None:
|
|
48
|
+
if not is_latin_1_encodable(username):
|
|
49
|
+
raise ConfigError("Username should be latin-1 encodable.")
|
|
50
|
+
if not is_latin_1_encodable(password):
|
|
51
|
+
raise ConfigError("Password should be latin-1 encodable.")
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Sequence
|
|
5
|
+
|
|
6
|
+
from schemathesis.config._diff_base import DiffBase
|
|
7
|
+
from schemathesis.config._error import ConfigError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
NOT_A_SERVER_ERROR_EXPECTED_STATUSES = ["2xx", "3xx", "4xx"]
|
|
13
|
+
NEGATIVE_DATA_REJECTION_EXPECTED_STATUSES = ["400", "401", "403", "404", "406", "422", "428", "5xx"]
|
|
14
|
+
POSITIVE_DATA_ACCEPTANCE_EXPECTED_STATUSES = ["2xx", "401", "403", "404", "5xx"]
|
|
15
|
+
MISSING_REQUIRED_HEADER_EXPECTED_STATUSES = ["406"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_status_codes(value: Sequence[str] | None) -> Sequence[str] | None:
|
|
19
|
+
if not value:
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
invalid = []
|
|
23
|
+
|
|
24
|
+
for code in value:
|
|
25
|
+
if len(code) != 3:
|
|
26
|
+
invalid.append(code)
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if code[0] not in {"1", "2", "3", "4", "5"}:
|
|
30
|
+
invalid.append(code)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
upper_code = code.upper()
|
|
34
|
+
|
|
35
|
+
if "X" in upper_code:
|
|
36
|
+
if (
|
|
37
|
+
upper_code[1:] == "XX"
|
|
38
|
+
or (upper_code[1] == "X" and upper_code[2].isdigit())
|
|
39
|
+
or (upper_code[1].isdigit() and upper_code[2] == "X")
|
|
40
|
+
):
|
|
41
|
+
continue
|
|
42
|
+
else:
|
|
43
|
+
invalid.append(code)
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if not code.isnumeric():
|
|
47
|
+
invalid.append(code)
|
|
48
|
+
|
|
49
|
+
if invalid:
|
|
50
|
+
raise ConfigError(
|
|
51
|
+
f"Invalid status code(s): {', '.join(invalid)}. "
|
|
52
|
+
"Use valid 3-digit codes between 100 and 599, "
|
|
53
|
+
"or wildcards (e.g., 2XX, 2X0, 20X), where X is a wildcard digit."
|
|
54
|
+
)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(repr=False)
|
|
59
|
+
class SimpleCheckConfig(DiffBase):
|
|
60
|
+
enabled: bool
|
|
61
|
+
|
|
62
|
+
__slots__ = ("enabled",)
|
|
63
|
+
|
|
64
|
+
def __init__(self, *, enabled: bool = True) -> None:
|
|
65
|
+
self.enabled = enabled
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: dict[str, Any]) -> SimpleCheckConfig:
|
|
69
|
+
return cls(enabled=data.get("enabled", True))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(repr=False)
|
|
73
|
+
class MaxResponseTimeConfig(DiffBase):
|
|
74
|
+
enabled: bool
|
|
75
|
+
limit: float | None
|
|
76
|
+
|
|
77
|
+
__slots__ = ("enabled", "limit")
|
|
78
|
+
|
|
79
|
+
def __init__(self, *, limit: float | None = None) -> None:
|
|
80
|
+
self.enabled = limit is not None
|
|
81
|
+
self.limit = limit
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(repr=False)
|
|
85
|
+
class CheckConfig(DiffBase):
|
|
86
|
+
enabled: bool
|
|
87
|
+
expected_statuses: list[str]
|
|
88
|
+
_DEFAULT_EXPECTED_STATUSES: ClassVar[list[str]]
|
|
89
|
+
|
|
90
|
+
__slots__ = ("enabled", "expected_statuses")
|
|
91
|
+
|
|
92
|
+
def __init__(self, *, enabled: bool = True, expected_statuses: Sequence[str | int] | None = None) -> None:
|
|
93
|
+
self.enabled = enabled
|
|
94
|
+
if expected_statuses is not None:
|
|
95
|
+
statuses = [str(status) for status in expected_statuses]
|
|
96
|
+
validate_status_codes(statuses)
|
|
97
|
+
self.expected_statuses = statuses
|
|
98
|
+
else:
|
|
99
|
+
self.expected_statuses = self._DEFAULT_EXPECTED_STATUSES
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
103
|
+
enabled = data.get("enabled", True)
|
|
104
|
+
return cls(
|
|
105
|
+
enabled=enabled,
|
|
106
|
+
expected_statuses=data.get("expected-statuses", cls._DEFAULT_EXPECTED_STATUSES),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class NotAServerErrorConfig(CheckConfig):
|
|
111
|
+
_DEFAULT_EXPECTED_STATUSES = NOT_A_SERVER_ERROR_EXPECTED_STATUSES
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PositiveDataAcceptanceConfig(CheckConfig):
|
|
115
|
+
_DEFAULT_EXPECTED_STATUSES = POSITIVE_DATA_ACCEPTANCE_EXPECTED_STATUSES
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NegativeDataRejectionConfig(CheckConfig):
|
|
119
|
+
_DEFAULT_EXPECTED_STATUSES = NEGATIVE_DATA_REJECTION_EXPECTED_STATUSES
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MissingRequiredHeaderConfig(CheckConfig):
|
|
123
|
+
_DEFAULT_EXPECTED_STATUSES = MISSING_REQUIRED_HEADER_EXPECTED_STATUSES
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(repr=False)
|
|
127
|
+
class ChecksConfig(DiffBase):
|
|
128
|
+
not_a_server_error: NotAServerErrorConfig
|
|
129
|
+
status_code_conformance: SimpleCheckConfig
|
|
130
|
+
content_type_conformance: SimpleCheckConfig
|
|
131
|
+
response_schema_conformance: SimpleCheckConfig
|
|
132
|
+
response_headers_conformance: SimpleCheckConfig
|
|
133
|
+
positive_data_acceptance: PositiveDataAcceptanceConfig
|
|
134
|
+
negative_data_rejection: NegativeDataRejectionConfig
|
|
135
|
+
use_after_free: SimpleCheckConfig
|
|
136
|
+
ensure_resource_availability: SimpleCheckConfig
|
|
137
|
+
missing_required_header: MissingRequiredHeaderConfig
|
|
138
|
+
ignored_auth: SimpleCheckConfig
|
|
139
|
+
unsupported_method: SimpleCheckConfig
|
|
140
|
+
max_response_time: MaxResponseTimeConfig
|
|
141
|
+
_unknown: dict[str, SimpleCheckConfig]
|
|
142
|
+
|
|
143
|
+
__slots__ = (
|
|
144
|
+
"not_a_server_error",
|
|
145
|
+
"status_code_conformance",
|
|
146
|
+
"content_type_conformance",
|
|
147
|
+
"response_schema_conformance",
|
|
148
|
+
"response_headers_conformance",
|
|
149
|
+
"positive_data_acceptance",
|
|
150
|
+
"negative_data_rejection",
|
|
151
|
+
"use_after_free",
|
|
152
|
+
"ensure_resource_availability",
|
|
153
|
+
"missing_required_header",
|
|
154
|
+
"ignored_auth",
|
|
155
|
+
"unsupported_method",
|
|
156
|
+
"max_response_time",
|
|
157
|
+
"_unknown",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
not_a_server_error: NotAServerErrorConfig | None = None,
|
|
164
|
+
status_code_conformance: SimpleCheckConfig | None = None,
|
|
165
|
+
content_type_conformance: SimpleCheckConfig | None = None,
|
|
166
|
+
response_schema_conformance: SimpleCheckConfig | None = None,
|
|
167
|
+
response_headers_conformance: SimpleCheckConfig | None = None,
|
|
168
|
+
positive_data_acceptance: PositiveDataAcceptanceConfig | None = None,
|
|
169
|
+
negative_data_rejection: NegativeDataRejectionConfig | None = None,
|
|
170
|
+
use_after_free: SimpleCheckConfig | None = None,
|
|
171
|
+
ensure_resource_availability: SimpleCheckConfig | None = None,
|
|
172
|
+
missing_required_header: MissingRequiredHeaderConfig | None = None,
|
|
173
|
+
ignored_auth: SimpleCheckConfig | None = None,
|
|
174
|
+
unsupported_method: SimpleCheckConfig | None = None,
|
|
175
|
+
max_response_time: MaxResponseTimeConfig | None = None,
|
|
176
|
+
) -> None:
|
|
177
|
+
self.not_a_server_error = not_a_server_error or NotAServerErrorConfig()
|
|
178
|
+
self.status_code_conformance = status_code_conformance or SimpleCheckConfig()
|
|
179
|
+
self.content_type_conformance = content_type_conformance or SimpleCheckConfig()
|
|
180
|
+
self.response_schema_conformance = response_schema_conformance or SimpleCheckConfig()
|
|
181
|
+
self.response_headers_conformance = response_headers_conformance or SimpleCheckConfig()
|
|
182
|
+
self.positive_data_acceptance = positive_data_acceptance or PositiveDataAcceptanceConfig()
|
|
183
|
+
self.negative_data_rejection = negative_data_rejection or NegativeDataRejectionConfig()
|
|
184
|
+
self.use_after_free = use_after_free or SimpleCheckConfig()
|
|
185
|
+
self.ensure_resource_availability = ensure_resource_availability or SimpleCheckConfig()
|
|
186
|
+
self.missing_required_header = missing_required_header or MissingRequiredHeaderConfig()
|
|
187
|
+
self.ignored_auth = ignored_auth or SimpleCheckConfig()
|
|
188
|
+
self.unsupported_method = unsupported_method or SimpleCheckConfig()
|
|
189
|
+
self.max_response_time = max_response_time or MaxResponseTimeConfig()
|
|
190
|
+
self._unknown = {}
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_dict(cls, data: dict[str, Any]) -> ChecksConfig:
|
|
194
|
+
# Use the outer "enabled" value as default for all checks.
|
|
195
|
+
default_enabled = data.get("enabled", None)
|
|
196
|
+
|
|
197
|
+
def merge(sub: dict[str, Any]) -> dict[str, Any]:
|
|
198
|
+
# Merge the default enabled flag with the sub-dict; the sub-dict takes precedence.
|
|
199
|
+
if default_enabled is not None:
|
|
200
|
+
return {"enabled": default_enabled, **sub}
|
|
201
|
+
return sub
|
|
202
|
+
|
|
203
|
+
return cls(
|
|
204
|
+
not_a_server_error=NotAServerErrorConfig.from_dict(
|
|
205
|
+
merge(data.get("not_a_server_error", {})),
|
|
206
|
+
),
|
|
207
|
+
status_code_conformance=SimpleCheckConfig.from_dict(merge(data.get("status_code_conformance", {}))),
|
|
208
|
+
content_type_conformance=SimpleCheckConfig.from_dict(merge(data.get("content_type_conformance", {}))),
|
|
209
|
+
response_schema_conformance=SimpleCheckConfig.from_dict(merge(data.get("response_schema_conformance", {}))),
|
|
210
|
+
response_headers_conformance=SimpleCheckConfig.from_dict(
|
|
211
|
+
merge(data.get("response_headers_conformance", {}))
|
|
212
|
+
),
|
|
213
|
+
positive_data_acceptance=PositiveDataAcceptanceConfig.from_dict(
|
|
214
|
+
merge(data.get("positive_data_acceptance", {})),
|
|
215
|
+
),
|
|
216
|
+
negative_data_rejection=NegativeDataRejectionConfig.from_dict(
|
|
217
|
+
merge(data.get("negative_data_rejection", {})),
|
|
218
|
+
),
|
|
219
|
+
use_after_free=SimpleCheckConfig.from_dict(merge(data.get("use_after_free", {}))),
|
|
220
|
+
ensure_resource_availability=SimpleCheckConfig.from_dict(
|
|
221
|
+
merge(data.get("ensure_resource_availability", {}))
|
|
222
|
+
),
|
|
223
|
+
missing_required_header=MissingRequiredHeaderConfig.from_dict(
|
|
224
|
+
merge(data.get("missing_required_header", {})),
|
|
225
|
+
),
|
|
226
|
+
ignored_auth=SimpleCheckConfig.from_dict(merge(data.get("ignored_auth", {}))),
|
|
227
|
+
unsupported_method=SimpleCheckConfig.from_dict(merge(data.get("unsupported_method", {}))),
|
|
228
|
+
max_response_time=MaxResponseTimeConfig(limit=data.get("max_response_time")),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def get_by_name(self, *, name: str) -> CheckConfig | SimpleCheckConfig | MaxResponseTimeConfig:
|
|
232
|
+
try:
|
|
233
|
+
return getattr(self, name)
|
|
234
|
+
except AttributeError:
|
|
235
|
+
return self._unknown.setdefault(name, SimpleCheckConfig())
|
|
236
|
+
|
|
237
|
+
def update(
|
|
238
|
+
self,
|
|
239
|
+
*,
|
|
240
|
+
included_check_names: list[str] | None = None,
|
|
241
|
+
excluded_check_names: list[str] | None = None,
|
|
242
|
+
max_response_time: float | None = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
known_names = {name for name in self.__slots__ if not name.startswith("_")}
|
|
245
|
+
for name in known_names:
|
|
246
|
+
# Check in explicitly excluded or not in explicitly included
|
|
247
|
+
if name in (excluded_check_names or []) or (
|
|
248
|
+
included_check_names is not None
|
|
249
|
+
and "all" not in included_check_names
|
|
250
|
+
and name not in included_check_names
|
|
251
|
+
):
|
|
252
|
+
config = self.get_by_name(name=name)
|
|
253
|
+
config.enabled = False
|
|
254
|
+
elif included_check_names is not None and name in included_check_names:
|
|
255
|
+
config = self.get_by_name(name=name)
|
|
256
|
+
config.enabled = True
|
|
257
|
+
|
|
258
|
+
if max_response_time is not None:
|
|
259
|
+
self.max_response_time.enabled = True
|
|
260
|
+
self.max_response_time.limit = max_response_time
|
|
261
|
+
|
|
262
|
+
for name in included_check_names or []:
|
|
263
|
+
if name not in known_names and name != "all":
|
|
264
|
+
self._unknown[name] = SimpleCheckConfig(enabled=True)
|
|
265
|
+
|
|
266
|
+
for name in excluded_check_names or []:
|
|
267
|
+
if name not in known_names and name != "all":
|
|
268
|
+
self._unknown[name] = SimpleCheckConfig(enabled=False)
|