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.
Files changed (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {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