schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +523 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from schemathesis.config._checks import ChecksConfig
|
7
|
+
from schemathesis.config._diff_base import DiffBase
|
8
|
+
from schemathesis.config._generation import GenerationConfig
|
9
|
+
from schemathesis.core import DEFAULT_STATEFUL_STEP_COUNT
|
10
|
+
|
11
|
+
DEFAULT_UNEXPECTED_METHODS = {"get", "put", "post", "delete", "options", "patch", "trace"}
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass(repr=False)
|
15
|
+
class PhaseConfig(DiffBase):
|
16
|
+
enabled: bool
|
17
|
+
generation: GenerationConfig
|
18
|
+
checks: ChecksConfig
|
19
|
+
|
20
|
+
__slots__ = ("enabled", "generation", "checks")
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
*,
|
25
|
+
enabled: bool = True,
|
26
|
+
generation: GenerationConfig | None = None,
|
27
|
+
checks: ChecksConfig | None = None,
|
28
|
+
) -> None:
|
29
|
+
self.enabled = enabled
|
30
|
+
self.generation = generation or GenerationConfig()
|
31
|
+
self.checks = checks or ChecksConfig()
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def from_dict(cls, data: dict[str, Any]) -> PhaseConfig:
|
35
|
+
return cls(
|
36
|
+
enabled=data.get("enabled", True),
|
37
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
38
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
@dataclass(repr=False)
|
43
|
+
class ExamplesPhaseConfig(DiffBase):
|
44
|
+
enabled: bool
|
45
|
+
fill_missing: bool
|
46
|
+
generation: GenerationConfig
|
47
|
+
checks: ChecksConfig
|
48
|
+
|
49
|
+
__slots__ = ("enabled", "fill_missing", "generation", "checks")
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
*,
|
54
|
+
enabled: bool = True,
|
55
|
+
fill_missing: bool = False,
|
56
|
+
generation: GenerationConfig | None = None,
|
57
|
+
checks: ChecksConfig | None = None,
|
58
|
+
) -> None:
|
59
|
+
self.enabled = enabled
|
60
|
+
self.fill_missing = fill_missing
|
61
|
+
self.generation = generation or GenerationConfig()
|
62
|
+
self.checks = checks or ChecksConfig()
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def from_dict(cls, data: dict[str, Any]) -> ExamplesPhaseConfig:
|
66
|
+
return cls(
|
67
|
+
enabled=data.get("enabled", True),
|
68
|
+
fill_missing=data.get("fill-missing", False),
|
69
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
70
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
71
|
+
)
|
72
|
+
|
73
|
+
|
74
|
+
@dataclass(repr=False)
|
75
|
+
class CoveragePhaseConfig(DiffBase):
|
76
|
+
enabled: bool
|
77
|
+
generate_duplicate_query_parameters: bool
|
78
|
+
generation: GenerationConfig
|
79
|
+
checks: ChecksConfig
|
80
|
+
unexpected_methods: set[str]
|
81
|
+
|
82
|
+
__slots__ = ("enabled", "generate_duplicate_query_parameters", "generation", "checks", "unexpected_methods")
|
83
|
+
|
84
|
+
def __init__(
|
85
|
+
self,
|
86
|
+
*,
|
87
|
+
enabled: bool = True,
|
88
|
+
generate_duplicate_query_parameters: bool = False,
|
89
|
+
generation: GenerationConfig | None = None,
|
90
|
+
checks: ChecksConfig | None = None,
|
91
|
+
unexpected_methods: set[str] | None = None,
|
92
|
+
) -> None:
|
93
|
+
self.enabled = enabled
|
94
|
+
self.generate_duplicate_query_parameters = generate_duplicate_query_parameters
|
95
|
+
self.unexpected_methods = unexpected_methods or DEFAULT_UNEXPECTED_METHODS
|
96
|
+
self.generation = generation or GenerationConfig()
|
97
|
+
self.checks = checks or ChecksConfig()
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def from_dict(cls, data: dict[str, Any]) -> CoveragePhaseConfig:
|
101
|
+
return cls(
|
102
|
+
enabled=data.get("enabled", True),
|
103
|
+
generate_duplicate_query_parameters=data.get("generate-duplicate-query-parameters", False),
|
104
|
+
unexpected_methods={method.lower() for method in data.get("unexpected-methods", [])}
|
105
|
+
if "unexpected-methods" in data
|
106
|
+
else None,
|
107
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
108
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
109
|
+
)
|
110
|
+
|
111
|
+
|
112
|
+
@dataclass(repr=False)
|
113
|
+
class StatefulPhaseConfig(DiffBase):
|
114
|
+
enabled: bool
|
115
|
+
generation: GenerationConfig
|
116
|
+
checks: ChecksConfig
|
117
|
+
max_steps: int
|
118
|
+
|
119
|
+
__slots__ = ("enabled", "generation", "checks", "max_steps")
|
120
|
+
|
121
|
+
def __init__(
|
122
|
+
self,
|
123
|
+
*,
|
124
|
+
enabled: bool = True,
|
125
|
+
generation: GenerationConfig | None = None,
|
126
|
+
checks: ChecksConfig | None = None,
|
127
|
+
max_steps: int | None = None,
|
128
|
+
) -> None:
|
129
|
+
self.enabled = enabled
|
130
|
+
self.max_steps = max_steps or DEFAULT_STATEFUL_STEP_COUNT
|
131
|
+
self.generation = generation or GenerationConfig()
|
132
|
+
self.checks = checks or ChecksConfig()
|
133
|
+
|
134
|
+
@classmethod
|
135
|
+
def from_dict(cls, data: dict[str, Any]) -> StatefulPhaseConfig:
|
136
|
+
return cls(
|
137
|
+
enabled=data.get("enabled", True),
|
138
|
+
max_steps=data.get("max-steps"),
|
139
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
140
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
141
|
+
)
|
142
|
+
|
143
|
+
|
144
|
+
@dataclass(repr=False)
|
145
|
+
class PhasesConfig(DiffBase):
|
146
|
+
examples: ExamplesPhaseConfig
|
147
|
+
coverage: CoveragePhaseConfig
|
148
|
+
fuzzing: PhaseConfig
|
149
|
+
stateful: StatefulPhaseConfig
|
150
|
+
|
151
|
+
__slots__ = ("examples", "coverage", "fuzzing", "stateful")
|
152
|
+
|
153
|
+
def __init__(
|
154
|
+
self,
|
155
|
+
*,
|
156
|
+
examples: ExamplesPhaseConfig | None = None,
|
157
|
+
coverage: CoveragePhaseConfig | None = None,
|
158
|
+
fuzzing: PhaseConfig | None = None,
|
159
|
+
stateful: StatefulPhaseConfig | None = None,
|
160
|
+
) -> None:
|
161
|
+
self.examples = examples or ExamplesPhaseConfig()
|
162
|
+
self.coverage = coverage or CoveragePhaseConfig()
|
163
|
+
self.fuzzing = fuzzing or PhaseConfig()
|
164
|
+
self.stateful = stateful or StatefulPhaseConfig()
|
165
|
+
|
166
|
+
def get_by_name(self, *, name: str) -> PhaseConfig | CoveragePhaseConfig | StatefulPhaseConfig:
|
167
|
+
return {
|
168
|
+
"examples": self.examples,
|
169
|
+
"coverage": self.coverage,
|
170
|
+
"fuzzing": self.fuzzing,
|
171
|
+
"stateful": self.stateful,
|
172
|
+
}[name] # type: ignore[return-value]
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def from_dict(cls, data: dict[str, Any]) -> PhasesConfig:
|
176
|
+
return cls(
|
177
|
+
examples=ExamplesPhaseConfig.from_dict(data.get("examples", {})),
|
178
|
+
coverage=CoveragePhaseConfig.from_dict(data.get("coverage", {})),
|
179
|
+
fuzzing=PhaseConfig.from_dict(data.get("fuzzing", {})),
|
180
|
+
stateful=StatefulPhaseConfig.from_dict(data.get("stateful", {})),
|
181
|
+
)
|
182
|
+
|
183
|
+
def update(self, *, phases: list[str]) -> None:
|
184
|
+
self.examples.enabled = "examples" in phases
|
185
|
+
self.coverage.enabled = "coverage" in phases
|
186
|
+
self.fuzzing.enabled = "fuzzing" in phases
|
187
|
+
self.stateful.enabled = "stateful" in phases
|
@@ -0,0 +1,523 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
6
|
+
|
7
|
+
from schemathesis.config._auth import AuthConfig
|
8
|
+
from schemathesis.config._checks import ChecksConfig
|
9
|
+
from schemathesis.config._diff_base import DiffBase
|
10
|
+
from schemathesis.config._env import resolve
|
11
|
+
from schemathesis.config._error import ConfigError
|
12
|
+
from schemathesis.config._generation import GenerationConfig
|
13
|
+
from schemathesis.config._health_check import HealthCheck
|
14
|
+
from schemathesis.config._operations import OperationConfig, OperationsConfig
|
15
|
+
from schemathesis.config._output import OutputConfig
|
16
|
+
from schemathesis.config._parameters import load_parameters
|
17
|
+
from schemathesis.config._phases import PhasesConfig
|
18
|
+
from schemathesis.config._rate_limit import build_limiter
|
19
|
+
from schemathesis.config._report import ReportsConfig
|
20
|
+
from schemathesis.config._warnings import SchemathesisWarning, resolve_warnings
|
21
|
+
from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER, hooks
|
22
|
+
from schemathesis.core.validation import validate_base_url
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
import hypothesis
|
26
|
+
from pyrate_limiter import Limiter
|
27
|
+
|
28
|
+
from schemathesis.config import SchemathesisConfig
|
29
|
+
from schemathesis.schemas import APIOperation
|
30
|
+
|
31
|
+
DEFAULT_WORKERS = 1
|
32
|
+
|
33
|
+
|
34
|
+
def get_workers_count() -> int:
|
35
|
+
"""Detect the number of available CPUs for the current process, if possible.
|
36
|
+
|
37
|
+
Use ``DEFAULT_WORKERS`` if not possible to detect.
|
38
|
+
"""
|
39
|
+
if hasattr(os, "sched_getaffinity"):
|
40
|
+
# In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
|
41
|
+
return len(os.sched_getaffinity(0))
|
42
|
+
# Number of CPUs in the system, or 1 if undetermined
|
43
|
+
return os.cpu_count() or DEFAULT_WORKERS
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass(repr=False)
|
47
|
+
class ProjectConfig(DiffBase):
|
48
|
+
_parent: SchemathesisConfig | None
|
49
|
+
base_url: str | None
|
50
|
+
headers: dict | None
|
51
|
+
hooks: str | None
|
52
|
+
proxy: str | None
|
53
|
+
workers: int
|
54
|
+
continue_on_failure: bool | None
|
55
|
+
tls_verify: bool | str | None
|
56
|
+
rate_limit: Limiter | None
|
57
|
+
request_timeout: float | int | None
|
58
|
+
request_cert: str | None
|
59
|
+
request_cert_key: str | None
|
60
|
+
parameters: dict[str, Any]
|
61
|
+
warnings: list[SchemathesisWarning] | None
|
62
|
+
auth: AuthConfig
|
63
|
+
checks: ChecksConfig
|
64
|
+
phases: PhasesConfig
|
65
|
+
generation: GenerationConfig
|
66
|
+
operations: OperationsConfig
|
67
|
+
|
68
|
+
__slots__ = (
|
69
|
+
"_parent",
|
70
|
+
"base_url",
|
71
|
+
"headers",
|
72
|
+
"hooks",
|
73
|
+
"proxy",
|
74
|
+
"workers",
|
75
|
+
"continue_on_failure",
|
76
|
+
"tls_verify",
|
77
|
+
"rate_limit",
|
78
|
+
"_rate_limit",
|
79
|
+
"request_timeout",
|
80
|
+
"request_cert",
|
81
|
+
"request_cert_key",
|
82
|
+
"parameters",
|
83
|
+
"warnings",
|
84
|
+
"auth",
|
85
|
+
"checks",
|
86
|
+
"phases",
|
87
|
+
"generation",
|
88
|
+
"operations",
|
89
|
+
)
|
90
|
+
|
91
|
+
def __init__(
|
92
|
+
self,
|
93
|
+
*,
|
94
|
+
parent: SchemathesisConfig | None = None,
|
95
|
+
base_url: str | None = None,
|
96
|
+
headers: dict | None = None,
|
97
|
+
hooks_: str | None = None,
|
98
|
+
workers: int | Literal["auto"] = DEFAULT_WORKERS,
|
99
|
+
proxy: str | None = None,
|
100
|
+
continue_on_failure: bool | None = None,
|
101
|
+
tls_verify: bool | str | None = None,
|
102
|
+
rate_limit: str | None = None,
|
103
|
+
request_timeout: float | int | None = None,
|
104
|
+
request_cert: str | None = None,
|
105
|
+
request_cert_key: str | None = None,
|
106
|
+
parameters: dict[str, Any] | None = None,
|
107
|
+
warnings: bool | list[SchemathesisWarning] | None = None,
|
108
|
+
auth: AuthConfig | None = None,
|
109
|
+
checks: ChecksConfig | None = None,
|
110
|
+
phases: PhasesConfig | None = None,
|
111
|
+
generation: GenerationConfig | None = None,
|
112
|
+
operations: OperationsConfig | None = None,
|
113
|
+
) -> None:
|
114
|
+
self._parent = parent
|
115
|
+
if base_url is not None:
|
116
|
+
_validate_base_url(base_url)
|
117
|
+
self.base_url = base_url
|
118
|
+
self.headers = headers
|
119
|
+
self.hooks = hooks_
|
120
|
+
if hooks_:
|
121
|
+
hooks.load_from_path(hooks_)
|
122
|
+
else:
|
123
|
+
hooks.load_from_env()
|
124
|
+
if isinstance(workers, int):
|
125
|
+
self.workers = workers
|
126
|
+
else:
|
127
|
+
self.workers = get_workers_count()
|
128
|
+
self.proxy = proxy
|
129
|
+
self.continue_on_failure = continue_on_failure
|
130
|
+
self.tls_verify = tls_verify
|
131
|
+
if rate_limit is not None:
|
132
|
+
self.rate_limit = build_limiter(rate_limit)
|
133
|
+
else:
|
134
|
+
self.rate_limit = rate_limit
|
135
|
+
self._rate_limit = rate_limit
|
136
|
+
self.request_timeout = request_timeout
|
137
|
+
self.request_cert = request_cert
|
138
|
+
self.request_cert_key = request_cert_key
|
139
|
+
self.parameters = parameters or {}
|
140
|
+
self._set_warnings(warnings)
|
141
|
+
self.auth = auth or AuthConfig()
|
142
|
+
self.checks = checks or ChecksConfig()
|
143
|
+
self.phases = phases or PhasesConfig()
|
144
|
+
self.generation = generation or GenerationConfig()
|
145
|
+
self.operations = operations or OperationsConfig()
|
146
|
+
|
147
|
+
@classmethod
|
148
|
+
def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
|
149
|
+
return cls(
|
150
|
+
base_url=resolve(data.get("base-url")),
|
151
|
+
headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
|
152
|
+
if "headers" in data
|
153
|
+
else None,
|
154
|
+
hooks_=resolve(data.get("hooks")),
|
155
|
+
workers=data.get("workers", DEFAULT_WORKERS),
|
156
|
+
proxy=resolve(data.get("proxy")),
|
157
|
+
continue_on_failure=data.get("continue-on-failure", None),
|
158
|
+
tls_verify=resolve(data.get("tls-verify")),
|
159
|
+
rate_limit=resolve(data.get("rate-limit")),
|
160
|
+
request_timeout=data.get("request-timeout"),
|
161
|
+
request_cert=resolve(data.get("request-cert")),
|
162
|
+
request_cert_key=resolve(data.get("request-cert-key")),
|
163
|
+
parameters=load_parameters(data),
|
164
|
+
auth=AuthConfig.from_dict(data.get("auth", {})),
|
165
|
+
warnings=resolve_warnings(data.get("warnings")),
|
166
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
167
|
+
phases=PhasesConfig.from_dict(data.get("phases", {})),
|
168
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
169
|
+
operations=OperationsConfig(
|
170
|
+
operations=[OperationConfig.from_dict(operation) for operation in data.get("operations", [])]
|
171
|
+
),
|
172
|
+
)
|
173
|
+
|
174
|
+
def _set_warnings(self, warnings: bool | list[SchemathesisWarning] | None) -> None:
|
175
|
+
if warnings is False:
|
176
|
+
self.warnings = []
|
177
|
+
elif warnings is True:
|
178
|
+
self.warnings = list(SchemathesisWarning)
|
179
|
+
else:
|
180
|
+
self.warnings = warnings
|
181
|
+
|
182
|
+
def update(
|
183
|
+
self,
|
184
|
+
*,
|
185
|
+
base_url: str | None = None,
|
186
|
+
headers: dict | None = None,
|
187
|
+
basic_auth: tuple[str, str] | None = None,
|
188
|
+
workers: int | Literal["auto"] | None = None,
|
189
|
+
continue_on_failure: bool | None = None,
|
190
|
+
rate_limit: str | None = None,
|
191
|
+
request_timeout: float | int | None = None,
|
192
|
+
tls_verify: bool | str | None = None,
|
193
|
+
request_cert: str | None = None,
|
194
|
+
request_cert_key: str | None = None,
|
195
|
+
proxy: str | None = None,
|
196
|
+
suppress_health_check: list[HealthCheck] | None = None,
|
197
|
+
warnings: bool | list[SchemathesisWarning] | None = None,
|
198
|
+
) -> None:
|
199
|
+
if base_url is not None:
|
200
|
+
_validate_base_url(base_url)
|
201
|
+
self.base_url = base_url
|
202
|
+
|
203
|
+
if headers is not None:
|
204
|
+
_headers = self.headers or {}
|
205
|
+
_headers.update(headers)
|
206
|
+
self.headers = _headers
|
207
|
+
|
208
|
+
if basic_auth is not None:
|
209
|
+
self.auth.update(basic=basic_auth)
|
210
|
+
|
211
|
+
if workers is not None:
|
212
|
+
if isinstance(workers, int):
|
213
|
+
self.workers = workers
|
214
|
+
else:
|
215
|
+
self.workers = get_workers_count()
|
216
|
+
|
217
|
+
if continue_on_failure is not None:
|
218
|
+
self.continue_on_failure = continue_on_failure
|
219
|
+
|
220
|
+
if rate_limit is not None:
|
221
|
+
self.rate_limit = build_limiter(rate_limit)
|
222
|
+
|
223
|
+
if request_timeout is not None:
|
224
|
+
self.request_timeout = request_timeout
|
225
|
+
|
226
|
+
if tls_verify is not None:
|
227
|
+
self.tls_verify = tls_verify
|
228
|
+
|
229
|
+
if request_cert is not None:
|
230
|
+
self.request_cert = request_cert
|
231
|
+
|
232
|
+
if request_cert_key is not None:
|
233
|
+
self.request_cert_key = request_cert_key
|
234
|
+
|
235
|
+
if proxy is not None:
|
236
|
+
self.proxy = proxy
|
237
|
+
|
238
|
+
if suppress_health_check is not None:
|
239
|
+
self.suppress_health_check = suppress_health_check
|
240
|
+
|
241
|
+
if warnings is not None:
|
242
|
+
self._set_warnings(warnings)
|
243
|
+
|
244
|
+
def auth_for(self, *, operation: APIOperation | None = None) -> tuple[str, str] | None:
|
245
|
+
"""Get auth credentials, prioritizing operation-specific configs."""
|
246
|
+
if operation is not None:
|
247
|
+
config = self.operations.get_for_operation(operation=operation)
|
248
|
+
if config.auth.basic is not None:
|
249
|
+
return config.auth.basic
|
250
|
+
if self.auth.basic is not None:
|
251
|
+
return self.auth.basic
|
252
|
+
return None
|
253
|
+
|
254
|
+
def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str] | None:
|
255
|
+
"""Get explicitly configured headers."""
|
256
|
+
headers = self.headers.copy() if self.headers else {}
|
257
|
+
if operation is not None:
|
258
|
+
config = self.operations.get_for_operation(operation=operation)
|
259
|
+
if config.headers is not None:
|
260
|
+
headers.update(config.headers)
|
261
|
+
return headers
|
262
|
+
|
263
|
+
def request_timeout_for(self, *, operation: APIOperation | None = None) -> float | int | None:
|
264
|
+
if operation is not None:
|
265
|
+
config = self.operations.get_for_operation(operation=operation)
|
266
|
+
if config.request_timeout is not None:
|
267
|
+
return config.request_timeout
|
268
|
+
if self.request_timeout is not None:
|
269
|
+
return self.request_timeout
|
270
|
+
return None
|
271
|
+
|
272
|
+
def tls_verify_for(self, *, operation: APIOperation | None = None) -> bool | str | None:
|
273
|
+
if operation is not None:
|
274
|
+
config = self.operations.get_for_operation(operation=operation)
|
275
|
+
if config.tls_verify is not None:
|
276
|
+
return config.tls_verify
|
277
|
+
if self.tls_verify is not None:
|
278
|
+
return self.tls_verify
|
279
|
+
return None
|
280
|
+
|
281
|
+
def request_cert_for(self, *, operation: APIOperation | None = None) -> str | tuple[str, str] | None:
|
282
|
+
if operation is not None:
|
283
|
+
config = self.operations.get_for_operation(operation=operation)
|
284
|
+
if config.request_cert is not None:
|
285
|
+
if config.request_cert_key:
|
286
|
+
return (config.request_cert, config.request_cert_key)
|
287
|
+
return config.request_cert
|
288
|
+
if self.request_cert is not None:
|
289
|
+
if self.request_cert_key:
|
290
|
+
return (self.request_cert, self.request_cert_key)
|
291
|
+
return self.request_cert
|
292
|
+
return None
|
293
|
+
|
294
|
+
def proxy_for(self, *, operation: APIOperation | None = None) -> str | None:
|
295
|
+
if operation is not None:
|
296
|
+
config = self.operations.get_for_operation(operation=operation)
|
297
|
+
if config.proxy is not None:
|
298
|
+
return config.proxy
|
299
|
+
if self.proxy is not None:
|
300
|
+
return self.proxy
|
301
|
+
return None
|
302
|
+
|
303
|
+
def rate_limit_for(self, *, operation: APIOperation | None = None) -> Limiter | None:
|
304
|
+
if operation is not None:
|
305
|
+
config = self.operations.get_for_operation(operation=operation)
|
306
|
+
if config.rate_limit is not None:
|
307
|
+
return config.rate_limit
|
308
|
+
if self.rate_limit is not None:
|
309
|
+
return self.rate_limit
|
310
|
+
return None
|
311
|
+
|
312
|
+
def warnings_for(self, *, operation: APIOperation | None = None) -> list[SchemathesisWarning]:
|
313
|
+
# Operation can be absent on some non-fatal errors due to schema parsing
|
314
|
+
if operation is not None:
|
315
|
+
config = self.operations.get_for_operation(operation=operation)
|
316
|
+
if config.warnings is not None:
|
317
|
+
return config.warnings
|
318
|
+
if self.warnings is None:
|
319
|
+
return list(SchemathesisWarning)
|
320
|
+
return self.warnings
|
321
|
+
|
322
|
+
def phases_for(self, *, operation: APIOperation | None) -> PhasesConfig:
|
323
|
+
configs = []
|
324
|
+
if operation is not None:
|
325
|
+
for op in self.operations.operations:
|
326
|
+
if op._filter_set.applies_to(operation=operation):
|
327
|
+
configs.append(op.phases)
|
328
|
+
configs.append(self.phases)
|
329
|
+
return PhasesConfig.from_hierarchy(configs)
|
330
|
+
|
331
|
+
def generation_for(
|
332
|
+
self,
|
333
|
+
*,
|
334
|
+
operation: APIOperation | None = None,
|
335
|
+
phase: str | None = None,
|
336
|
+
) -> GenerationConfig:
|
337
|
+
configs = []
|
338
|
+
if operation is not None:
|
339
|
+
for op in self.operations.operations:
|
340
|
+
if op._filter_set.applies_to(operation=operation):
|
341
|
+
if phase is not None:
|
342
|
+
phase_config = op.phases.get_by_name(name=phase)
|
343
|
+
configs.append(phase_config.generation)
|
344
|
+
configs.append(op.generation)
|
345
|
+
if phase is not None:
|
346
|
+
phases = self.phases_for(operation=operation)
|
347
|
+
phase_config = phases.get_by_name(name=phase)
|
348
|
+
configs.append(phase_config.generation)
|
349
|
+
configs.append(self.generation)
|
350
|
+
return GenerationConfig.from_hierarchy(configs)
|
351
|
+
|
352
|
+
def checks_config_for(
|
353
|
+
self,
|
354
|
+
*,
|
355
|
+
operation: APIOperation | None = None,
|
356
|
+
phase: str | None = None,
|
357
|
+
) -> ChecksConfig:
|
358
|
+
configs = []
|
359
|
+
if operation is not None:
|
360
|
+
for op in self.operations.operations:
|
361
|
+
if op._filter_set.applies_to(operation=operation):
|
362
|
+
if phase is not None:
|
363
|
+
phase_config = op.phases.get_by_name(name=phase)
|
364
|
+
configs.append(phase_config.checks)
|
365
|
+
configs.append(op.checks)
|
366
|
+
if phase is not None:
|
367
|
+
phases = self.phases_for(operation=operation)
|
368
|
+
phase_config = phases.get_by_name(name=phase)
|
369
|
+
configs.append(phase_config.checks)
|
370
|
+
configs.append(self.checks)
|
371
|
+
return ChecksConfig.from_hierarchy(configs)
|
372
|
+
|
373
|
+
def get_hypothesis_settings(
|
374
|
+
self,
|
375
|
+
*,
|
376
|
+
operation: APIOperation | None = None,
|
377
|
+
phase: str | None = None,
|
378
|
+
) -> hypothesis.settings:
|
379
|
+
import hypothesis
|
380
|
+
from hypothesis.database import DirectoryBasedExampleDatabase, InMemoryExampleDatabase
|
381
|
+
|
382
|
+
config = self.generation_for(operation=operation, phase=phase)
|
383
|
+
kwargs: dict[str, Any] = {}
|
384
|
+
|
385
|
+
if config.max_examples is not None:
|
386
|
+
kwargs["max_examples"] = config.max_examples
|
387
|
+
phases = set(hypothesis.Phase) - {hypothesis.Phase.explain}
|
388
|
+
if config.no_shrink:
|
389
|
+
phases.discard(hypothesis.Phase.shrink)
|
390
|
+
database = config.database
|
391
|
+
if database is not None:
|
392
|
+
if database.lower() == "none":
|
393
|
+
kwargs["database"] = None
|
394
|
+
phases.discard(hypothesis.Phase.reuse)
|
395
|
+
elif database == HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER:
|
396
|
+
kwargs["database"] = InMemoryExampleDatabase()
|
397
|
+
else:
|
398
|
+
kwargs["database"] = DirectoryBasedExampleDatabase(database)
|
399
|
+
|
400
|
+
return hypothesis.settings(
|
401
|
+
derandomize=config.deterministic,
|
402
|
+
print_blob=False,
|
403
|
+
deadline=None,
|
404
|
+
verbosity=hypothesis.Verbosity.quiet,
|
405
|
+
suppress_health_check=[check for item in self.suppress_health_check for check in item.as_hypothesis()],
|
406
|
+
phases=phases,
|
407
|
+
# NOTE: Ignoring any operation-specific config as stateful tests are not operation-specific
|
408
|
+
stateful_step_count=self.phases.stateful.max_steps,
|
409
|
+
**kwargs,
|
410
|
+
)
|
411
|
+
|
412
|
+
def _get_parent(self) -> SchemathesisConfig:
|
413
|
+
if self._parent is None:
|
414
|
+
from schemathesis.config import SchemathesisConfig
|
415
|
+
|
416
|
+
self._parent = SchemathesisConfig.discover()
|
417
|
+
return self._parent
|
418
|
+
|
419
|
+
@property
|
420
|
+
def output(self) -> OutputConfig:
|
421
|
+
return self._get_parent().output
|
422
|
+
|
423
|
+
@property
|
424
|
+
def wait_for_schema(self) -> float | int | None:
|
425
|
+
return self._get_parent().wait_for_schema
|
426
|
+
|
427
|
+
@property
|
428
|
+
def max_failures(self) -> int | None:
|
429
|
+
return self._get_parent().max_failures
|
430
|
+
|
431
|
+
@max_failures.setter
|
432
|
+
def max_failures(self, value: int) -> None:
|
433
|
+
parent = self._get_parent()
|
434
|
+
parent.max_failures = value
|
435
|
+
|
436
|
+
@property
|
437
|
+
def reports(self) -> ReportsConfig:
|
438
|
+
return self._get_parent().reports
|
439
|
+
|
440
|
+
@property
|
441
|
+
def suppress_health_check(self) -> list[HealthCheck]:
|
442
|
+
return self._get_parent().suppress_health_check
|
443
|
+
|
444
|
+
@suppress_health_check.setter
|
445
|
+
def suppress_health_check(self, value: list[HealthCheck]) -> None:
|
446
|
+
parent = self._get_parent()
|
447
|
+
parent.suppress_health_check = value
|
448
|
+
|
449
|
+
@property
|
450
|
+
def seed(self) -> int:
|
451
|
+
return self._get_parent().seed
|
452
|
+
|
453
|
+
@seed.setter
|
454
|
+
def seed(self, value: int) -> None:
|
455
|
+
parent = self._get_parent()
|
456
|
+
parent._seed = value
|
457
|
+
|
458
|
+
|
459
|
+
def _validate_base_url(base_url: str) -> None:
|
460
|
+
try:
|
461
|
+
validate_base_url(base_url)
|
462
|
+
except ValueError as exc:
|
463
|
+
raise ConfigError(str(exc)) from None
|
464
|
+
|
465
|
+
|
466
|
+
@dataclass(repr=False)
|
467
|
+
class ProjectsConfig(DiffBase):
|
468
|
+
default: ProjectConfig
|
469
|
+
named: dict[str, ProjectConfig]
|
470
|
+
_override: ProjectConfig
|
471
|
+
|
472
|
+
__slots__ = ("default", "named", "_override")
|
473
|
+
|
474
|
+
def __init__(
|
475
|
+
self,
|
476
|
+
*,
|
477
|
+
default: ProjectConfig | None = None,
|
478
|
+
named: dict[str, ProjectConfig] | None = None,
|
479
|
+
) -> None:
|
480
|
+
self.default = default or ProjectConfig()
|
481
|
+
self.named = named or {}
|
482
|
+
|
483
|
+
@property
|
484
|
+
def override(self) -> ProjectConfig:
|
485
|
+
if not hasattr(self, "_override"):
|
486
|
+
self._override = ProjectConfig()
|
487
|
+
self._override._parent = self.default._parent
|
488
|
+
return self._override
|
489
|
+
|
490
|
+
@classmethod
|
491
|
+
def from_dict(cls, data: dict[str, Any]) -> ProjectsConfig:
|
492
|
+
return cls(
|
493
|
+
default=ProjectConfig.from_dict(data),
|
494
|
+
named={project["title"]: ProjectConfig.from_dict(project) for project in data.get("project", [])},
|
495
|
+
)
|
496
|
+
|
497
|
+
def _set_parent(self, parent: SchemathesisConfig) -> None:
|
498
|
+
self.default._parent = parent
|
499
|
+
for project in self.named.values():
|
500
|
+
project._parent = parent
|
501
|
+
|
502
|
+
def get_default(self) -> ProjectConfig:
|
503
|
+
config = ProjectConfig.from_hierarchy([self.override, self.default])
|
504
|
+
config._parent = self.default._parent
|
505
|
+
return config
|
506
|
+
|
507
|
+
def get(self, schema: dict[str, Any]) -> ProjectConfig:
|
508
|
+
# Highest priority goes to `override`, then config specifically
|
509
|
+
# for the given project, then the "default" project config
|
510
|
+
configs = []
|
511
|
+
if hasattr(self, "_override"):
|
512
|
+
configs.append(self._override)
|
513
|
+
title = schema.get("info", {}).get("title")
|
514
|
+
if title is not None:
|
515
|
+
named = self.named.get(title)
|
516
|
+
if named is not None:
|
517
|
+
configs.append(named)
|
518
|
+
if not configs:
|
519
|
+
return self.default
|
520
|
+
configs.append(self.default)
|
521
|
+
config = ProjectConfig.from_hierarchy(configs)
|
522
|
+
config._parent = self.default._parent
|
523
|
+
return config
|