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