schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,527 @@
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
+ parameters: dict[str, Any] | None = None,
196
+ proxy: str | None = None,
197
+ suppress_health_check: list[HealthCheck] | None = None,
198
+ warnings: bool | list[SchemathesisWarning] | None = None,
199
+ ) -> None:
200
+ if base_url is not None:
201
+ _validate_base_url(base_url)
202
+ self.base_url = base_url
203
+
204
+ if headers is not None:
205
+ _headers = self.headers or {}
206
+ _headers.update(headers)
207
+ self.headers = _headers
208
+
209
+ if basic_auth is not None:
210
+ self.auth.update(basic=basic_auth)
211
+
212
+ if workers is not None:
213
+ if isinstance(workers, int):
214
+ self.workers = workers
215
+ else:
216
+ self.workers = get_workers_count()
217
+
218
+ if continue_on_failure is not None:
219
+ self.continue_on_failure = continue_on_failure
220
+
221
+ if rate_limit is not None:
222
+ self.rate_limit = build_limiter(rate_limit)
223
+
224
+ if request_timeout is not None:
225
+ self.request_timeout = request_timeout
226
+
227
+ if tls_verify is not None:
228
+ self.tls_verify = tls_verify
229
+
230
+ if request_cert is not None:
231
+ self.request_cert = request_cert
232
+
233
+ if request_cert_key is not None:
234
+ self.request_cert_key = request_cert_key
235
+
236
+ if proxy is not None:
237
+ self.proxy = proxy
238
+
239
+ if parameters is not None:
240
+ self.parameters = parameters
241
+
242
+ if suppress_health_check is not None:
243
+ self.suppress_health_check = suppress_health_check
244
+
245
+ if warnings is not None:
246
+ self._set_warnings(warnings)
247
+
248
+ def auth_for(self, *, operation: APIOperation | None = None) -> tuple[str, str] | None:
249
+ """Get auth credentials, prioritizing operation-specific configs."""
250
+ if operation is not None:
251
+ config = self.operations.get_for_operation(operation=operation)
252
+ if config.auth.basic is not None:
253
+ return config.auth.basic
254
+ if self.auth.basic is not None:
255
+ return self.auth.basic
256
+ return None
257
+
258
+ def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str]:
259
+ """Get explicitly configured headers."""
260
+ headers = self.headers.copy() if self.headers else {}
261
+ if operation is not None:
262
+ config = self.operations.get_for_operation(operation=operation)
263
+ if config.headers is not None:
264
+ headers.update(config.headers)
265
+ return headers
266
+
267
+ def request_timeout_for(self, *, operation: APIOperation | None = None) -> float | int | None:
268
+ if operation is not None:
269
+ config = self.operations.get_for_operation(operation=operation)
270
+ if config.request_timeout is not None:
271
+ return config.request_timeout
272
+ if self.request_timeout is not None:
273
+ return self.request_timeout
274
+ return None
275
+
276
+ def tls_verify_for(self, *, operation: APIOperation | None = None) -> bool | str | None:
277
+ if operation is not None:
278
+ config = self.operations.get_for_operation(operation=operation)
279
+ if config.tls_verify is not None:
280
+ return config.tls_verify
281
+ if self.tls_verify is not None:
282
+ return self.tls_verify
283
+ return None
284
+
285
+ def request_cert_for(self, *, operation: APIOperation | None = None) -> str | tuple[str, str] | None:
286
+ if operation is not None:
287
+ config = self.operations.get_for_operation(operation=operation)
288
+ if config.request_cert is not None:
289
+ if config.request_cert_key:
290
+ return (config.request_cert, config.request_cert_key)
291
+ return config.request_cert
292
+ if self.request_cert is not None:
293
+ if self.request_cert_key:
294
+ return (self.request_cert, self.request_cert_key)
295
+ return self.request_cert
296
+ return None
297
+
298
+ def proxy_for(self, *, operation: APIOperation | None = None) -> str | None:
299
+ if operation is not None:
300
+ config = self.operations.get_for_operation(operation=operation)
301
+ if config.proxy is not None:
302
+ return config.proxy
303
+ if self.proxy is not None:
304
+ return self.proxy
305
+ return None
306
+
307
+ def rate_limit_for(self, *, operation: APIOperation | None = None) -> Limiter | None:
308
+ if operation is not None:
309
+ config = self.operations.get_for_operation(operation=operation)
310
+ if config.rate_limit is not None:
311
+ return config.rate_limit
312
+ if self.rate_limit is not None:
313
+ return self.rate_limit
314
+ return None
315
+
316
+ def warnings_for(self, *, operation: APIOperation | None = None) -> list[SchemathesisWarning]:
317
+ # Operation can be absent on some non-fatal errors due to schema parsing
318
+ if operation is not None:
319
+ config = self.operations.get_for_operation(operation=operation)
320
+ if config.warnings is not None:
321
+ return config.warnings
322
+ if self.warnings is None:
323
+ return list(SchemathesisWarning)
324
+ return self.warnings
325
+
326
+ def phases_for(self, *, operation: APIOperation | None) -> PhasesConfig:
327
+ configs = []
328
+ if operation is not None:
329
+ for op in self.operations.operations:
330
+ if op._filter_set.applies_to(operation=operation):
331
+ configs.append(op.phases)
332
+ configs.append(self.phases)
333
+ return PhasesConfig.from_hierarchy(configs)
334
+
335
+ def generation_for(
336
+ self,
337
+ *,
338
+ operation: APIOperation | None = None,
339
+ phase: str | None = None,
340
+ ) -> GenerationConfig:
341
+ configs = []
342
+ if operation is not None:
343
+ for op in self.operations.operations:
344
+ if op._filter_set.applies_to(operation=operation):
345
+ if phase is not None:
346
+ phase_config = op.phases.get_by_name(name=phase)
347
+ configs.append(phase_config.generation)
348
+ configs.append(op.generation)
349
+ if phase is not None:
350
+ phases = self.phases_for(operation=operation)
351
+ phase_config = phases.get_by_name(name=phase)
352
+ configs.append(phase_config.generation)
353
+ configs.append(self.generation)
354
+ return GenerationConfig.from_hierarchy(configs)
355
+
356
+ def checks_config_for(
357
+ self,
358
+ *,
359
+ operation: APIOperation | None = None,
360
+ phase: str | None = None,
361
+ ) -> ChecksConfig:
362
+ configs = []
363
+ if operation is not None:
364
+ for op in self.operations.operations:
365
+ if op._filter_set.applies_to(operation=operation):
366
+ if phase is not None:
367
+ phase_config = op.phases.get_by_name(name=phase)
368
+ configs.append(phase_config.checks)
369
+ configs.append(op.checks)
370
+ if phase is not None:
371
+ phases = self.phases_for(operation=operation)
372
+ phase_config = phases.get_by_name(name=phase)
373
+ configs.append(phase_config.checks)
374
+ configs.append(self.checks)
375
+ return ChecksConfig.from_hierarchy(configs)
376
+
377
+ def get_hypothesis_settings(
378
+ self,
379
+ *,
380
+ operation: APIOperation | None = None,
381
+ phase: str | None = None,
382
+ ) -> hypothesis.settings:
383
+ import hypothesis
384
+ from hypothesis.database import DirectoryBasedExampleDatabase, InMemoryExampleDatabase
385
+
386
+ config = self.generation_for(operation=operation, phase=phase)
387
+ kwargs: dict[str, Any] = {}
388
+
389
+ if config.max_examples is not None:
390
+ kwargs["max_examples"] = config.max_examples
391
+ phases = set(hypothesis.Phase) - {hypothesis.Phase.explain}
392
+ if config.no_shrink:
393
+ phases.discard(hypothesis.Phase.shrink)
394
+ database = config.database
395
+ if database is not None:
396
+ if database.lower() == "none":
397
+ kwargs["database"] = None
398
+ phases.discard(hypothesis.Phase.reuse)
399
+ elif database == HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER:
400
+ kwargs["database"] = InMemoryExampleDatabase()
401
+ else:
402
+ kwargs["database"] = DirectoryBasedExampleDatabase(database)
403
+
404
+ return hypothesis.settings(
405
+ derandomize=config.deterministic,
406
+ print_blob=False,
407
+ deadline=None,
408
+ verbosity=hypothesis.Verbosity.quiet,
409
+ suppress_health_check=[check for item in self.suppress_health_check for check in item.as_hypothesis()],
410
+ phases=phases,
411
+ # NOTE: Ignoring any operation-specific config as stateful tests are not operation-specific
412
+ stateful_step_count=self.phases.stateful.max_steps,
413
+ **kwargs,
414
+ )
415
+
416
+ def _get_parent(self) -> SchemathesisConfig:
417
+ if self._parent is None:
418
+ from schemathesis.config import SchemathesisConfig
419
+
420
+ self._parent = SchemathesisConfig.discover()
421
+ return self._parent
422
+
423
+ @property
424
+ def output(self) -> OutputConfig:
425
+ return self._get_parent().output
426
+
427
+ @property
428
+ def wait_for_schema(self) -> float | int | None:
429
+ return self._get_parent().wait_for_schema
430
+
431
+ @property
432
+ def max_failures(self) -> int | None:
433
+ return self._get_parent().max_failures
434
+
435
+ @max_failures.setter
436
+ def max_failures(self, value: int) -> None:
437
+ parent = self._get_parent()
438
+ parent.max_failures = value
439
+
440
+ @property
441
+ def reports(self) -> ReportsConfig:
442
+ return self._get_parent().reports
443
+
444
+ @property
445
+ def suppress_health_check(self) -> list[HealthCheck]:
446
+ return self._get_parent().suppress_health_check
447
+
448
+ @suppress_health_check.setter
449
+ def suppress_health_check(self, value: list[HealthCheck]) -> None:
450
+ parent = self._get_parent()
451
+ parent.suppress_health_check = value
452
+
453
+ @property
454
+ def seed(self) -> int:
455
+ return self._get_parent().seed
456
+
457
+ @seed.setter
458
+ def seed(self, value: int) -> None:
459
+ parent = self._get_parent()
460
+ parent._seed = value
461
+
462
+
463
+ def _validate_base_url(base_url: str) -> None:
464
+ try:
465
+ validate_base_url(base_url)
466
+ except ValueError as exc:
467
+ raise ConfigError(str(exc)) from None
468
+
469
+
470
+ @dataclass(repr=False)
471
+ class ProjectsConfig(DiffBase):
472
+ default: ProjectConfig
473
+ named: dict[str, ProjectConfig]
474
+ _override: ProjectConfig
475
+
476
+ __slots__ = ("default", "named", "_override")
477
+
478
+ def __init__(
479
+ self,
480
+ *,
481
+ default: ProjectConfig | None = None,
482
+ named: dict[str, ProjectConfig] | None = None,
483
+ ) -> None:
484
+ self.default = default or ProjectConfig()
485
+ self.named = named or {}
486
+
487
+ @property
488
+ def override(self) -> ProjectConfig:
489
+ if not hasattr(self, "_override"):
490
+ self._override = ProjectConfig()
491
+ self._override._parent = self.default._parent
492
+ return self._override
493
+
494
+ @classmethod
495
+ def from_dict(cls, data: dict[str, Any]) -> ProjectsConfig:
496
+ return cls(
497
+ default=ProjectConfig.from_dict(data),
498
+ named={project["title"]: ProjectConfig.from_dict(project) for project in data.get("project", [])},
499
+ )
500
+
501
+ def _set_parent(self, parent: SchemathesisConfig) -> None:
502
+ self.default._parent = parent
503
+ for project in self.named.values():
504
+ project._parent = parent
505
+
506
+ def get_default(self) -> ProjectConfig:
507
+ config = ProjectConfig.from_hierarchy([self.override, self.default])
508
+ config._parent = self.default._parent
509
+ return config
510
+
511
+ def get(self, schema: dict[str, Any]) -> ProjectConfig:
512
+ # Highest priority goes to `override`, then config specifically
513
+ # for the given project, then the "default" project config
514
+ configs = []
515
+ if hasattr(self, "_override"):
516
+ configs.append(self._override)
517
+ title = schema.get("info", {}).get("title")
518
+ if title is not None:
519
+ named = self.named.get(title)
520
+ if named is not None:
521
+ configs.append(named)
522
+ if not configs:
523
+ return self.default
524
+ configs.append(self.default)
525
+ config = ProjectConfig.from_hierarchy(configs)
526
+ config._parent = self.default._parent
527
+ 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,120 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from schemathesis.config._diff_base import DiffBase
10
+ from schemathesis.config._env import resolve
11
+
12
+ DEFAULT_REPORT_DIRECTORY = Path("./schemathesis-report")
13
+
14
+
15
+ class ReportFormat(str, Enum):
16
+ """Available report formats."""
17
+
18
+ JUNIT = "junit"
19
+ VCR = "vcr"
20
+ HAR = "har"
21
+
22
+ @property
23
+ def extension(self) -> str:
24
+ """File extension for this format."""
25
+ return {
26
+ self.JUNIT: "xml",
27
+ self.VCR: "yaml",
28
+ self.HAR: "json",
29
+ }[self]
30
+
31
+
32
+ @dataclass(repr=False)
33
+ class ReportConfig(DiffBase):
34
+ enabled: bool
35
+ path: Path | None
36
+
37
+ __slots__ = ("enabled", "path")
38
+
39
+ def __init__(self, *, enabled: bool = False, path: Path | None = None) -> None:
40
+ self.enabled = enabled
41
+ self.path = path
42
+
43
+ @classmethod
44
+ def from_dict(cls, data: dict[str, Any]) -> ReportConfig:
45
+ path = resolve(data.get("path"))
46
+ if path is not None:
47
+ return cls(enabled=True, path=Path(path))
48
+ enabled = data.get("enabled", False)
49
+ return cls(enabled=enabled, path=path)
50
+
51
+
52
+ @dataclass(repr=False)
53
+ class ReportsConfig(DiffBase):
54
+ directory: Path
55
+ preserve_bytes: bool
56
+ junit: ReportConfig
57
+ vcr: ReportConfig
58
+ har: ReportConfig
59
+ _timestamp: str
60
+
61
+ __slots__ = ("directory", "preserve_bytes", "junit", "vcr", "har", "_timestamp")
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ directory: str | None = None,
67
+ preserve_bytes: bool = False,
68
+ junit: ReportConfig | None = None,
69
+ vcr: ReportConfig | None = None,
70
+ har: ReportConfig | None = None,
71
+ ) -> None:
72
+ self.directory = Path(resolve(directory) or DEFAULT_REPORT_DIRECTORY)
73
+ self.preserve_bytes = preserve_bytes
74
+ self.junit = junit or ReportConfig()
75
+ self.vcr = vcr or ReportConfig()
76
+ self.har = har or ReportConfig()
77
+ self._timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ")
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: dict[str, Any]) -> ReportsConfig:
81
+ return cls(
82
+ directory=data.get("directory"),
83
+ preserve_bytes=data.get("preserve-bytes", False),
84
+ junit=ReportConfig.from_dict(data.get("junit", {})),
85
+ vcr=ReportConfig.from_dict(data.get("vcr", {})),
86
+ har=ReportConfig.from_dict(data.get("har", {})),
87
+ )
88
+
89
+ def update(
90
+ self,
91
+ *,
92
+ formats: list[ReportFormat] | None = None,
93
+ junit_path: str | None = None,
94
+ vcr_path: str | None = None,
95
+ har_path: str | None = None,
96
+ directory: Path = DEFAULT_REPORT_DIRECTORY,
97
+ preserve_bytes: bool = False,
98
+ ) -> None:
99
+ formats = formats or []
100
+ if junit_path is not None or ReportFormat.JUNIT in formats:
101
+ self.junit.enabled = True
102
+ self.junit.path = Path(junit_path) if junit_path is not None else junit_path
103
+ if vcr_path is not None or ReportFormat.VCR in formats:
104
+ self.vcr.enabled = True
105
+ self.vcr.path = Path(vcr_path) if vcr_path is not None else vcr_path
106
+ if har_path is not None or ReportFormat.HAR in formats:
107
+ self.har.enabled = True
108
+ self.har.path = Path(har_path) if har_path is not None else har_path
109
+ if directory != DEFAULT_REPORT_DIRECTORY:
110
+ self.directory = directory
111
+ if preserve_bytes is True:
112
+ self.preserve_bytes = preserve_bytes
113
+
114
+ def get_path(self, format: ReportFormat) -> Path:
115
+ """Get the final path for a specific format."""
116
+ report: ReportConfig = getattr(self, format.value)
117
+ if report.path is not None:
118
+ return report.path
119
+
120
+ return self.directory / f"{format.value}-{self._timestamp}.{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)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+ from schemathesis.config._env import resolve
6
+
7
+
8
+ class SchemathesisWarning(str, enum.Enum):
9
+ MISSING_AUTH = "missing_auth"
10
+ MISSING_TEST_DATA = "missing_test_data"
11
+ VALIDATION_MISMATCH = "validation_mismatch"
12
+
13
+ @classmethod
14
+ def from_str(cls, value: str) -> SchemathesisWarning:
15
+ return {
16
+ "missing_auth": cls.MISSING_AUTH,
17
+ "missing_test_data": cls.MISSING_TEST_DATA,
18
+ "validation_mismatch": cls.VALIDATION_MISMATCH,
19
+ }[value.lower()]
20
+
21
+
22
+ def resolve_warnings(value: bool | list[str] | None) -> bool | list[SchemathesisWarning] | None:
23
+ if isinstance(value, list):
24
+ return [SchemathesisWarning.from_str(resolve(item)) for item in value]
25
+ return value