schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,543 @@
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 WarningsConfig
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
+ max_redirects: int | None
58
+ request_timeout: float | int | None
59
+ request_cert: str | None
60
+ request_cert_key: str | None
61
+ parameters: dict[str, Any]
62
+ warnings: WarningsConfig
63
+ auth: AuthConfig
64
+ checks: ChecksConfig
65
+ phases: PhasesConfig
66
+ generation: GenerationConfig
67
+ operations: OperationsConfig
68
+
69
+ __slots__ = (
70
+ "_parent",
71
+ "base_url",
72
+ "headers",
73
+ "hooks",
74
+ "proxy",
75
+ "workers",
76
+ "continue_on_failure",
77
+ "tls_verify",
78
+ "rate_limit",
79
+ "_rate_limit",
80
+ "max_redirects",
81
+ "request_timeout",
82
+ "request_cert",
83
+ "request_cert_key",
84
+ "parameters",
85
+ "warnings",
86
+ "auth",
87
+ "checks",
88
+ "phases",
89
+ "generation",
90
+ "operations",
91
+ )
92
+
93
+ def __init__(
94
+ self,
95
+ *,
96
+ parent: SchemathesisConfig | None = None,
97
+ base_url: str | None = None,
98
+ headers: dict | None = None,
99
+ hooks_: str | None = None,
100
+ workers: int | Literal["auto"] = DEFAULT_WORKERS,
101
+ proxy: str | None = None,
102
+ continue_on_failure: bool | None = None,
103
+ tls_verify: bool | str = True,
104
+ rate_limit: str | None = None,
105
+ max_redirects: int | None = None,
106
+ request_timeout: float | int | None = None,
107
+ request_cert: str | None = None,
108
+ request_cert_key: str | None = None,
109
+ parameters: dict[str, Any] | None = None,
110
+ warnings: WarningsConfig | None = None,
111
+ auth: AuthConfig | None = None,
112
+ checks: ChecksConfig | None = None,
113
+ phases: PhasesConfig | None = None,
114
+ generation: GenerationConfig | None = None,
115
+ operations: OperationsConfig | None = None,
116
+ ) -> None:
117
+ self._parent = parent
118
+ if base_url is not None:
119
+ _validate_base_url(base_url)
120
+ self.base_url = base_url
121
+ self.headers = headers
122
+ self.hooks = hooks_
123
+ if hooks_:
124
+ hooks.load_from_path(hooks_)
125
+ else:
126
+ hooks.load_from_env()
127
+ if isinstance(workers, int):
128
+ self.workers = workers
129
+ else:
130
+ self.workers = get_workers_count()
131
+ self.proxy = proxy
132
+ self.continue_on_failure = continue_on_failure
133
+ self.tls_verify = tls_verify
134
+ if rate_limit is not None:
135
+ self.rate_limit = build_limiter(rate_limit)
136
+ else:
137
+ self.rate_limit = rate_limit
138
+ self._rate_limit = rate_limit
139
+ self.max_redirects = max_redirects
140
+ self.request_timeout = request_timeout
141
+ self.request_cert = request_cert
142
+ self.request_cert_key = request_cert_key
143
+ self.parameters = parameters or {}
144
+ self.warnings = warnings or WarningsConfig.from_value(None)
145
+ self.auth = auth or AuthConfig()
146
+ self.checks = checks or ChecksConfig()
147
+ self.phases = phases or PhasesConfig()
148
+ self.generation = generation or GenerationConfig()
149
+ self.operations = operations or OperationsConfig()
150
+
151
+ @classmethod
152
+ def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
153
+ return cls(
154
+ base_url=resolve(data.get("base-url")),
155
+ headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
156
+ if "headers" in data
157
+ else None,
158
+ hooks_=resolve(data.get("hooks")),
159
+ workers=data.get("workers", DEFAULT_WORKERS),
160
+ proxy=resolve(data.get("proxy")),
161
+ continue_on_failure=data.get("continue-on-failure", None),
162
+ tls_verify=resolve(data.get("tls-verify", True)),
163
+ rate_limit=resolve(data.get("rate-limit")),
164
+ max_redirects=data.get("max-redirects"),
165
+ request_timeout=data.get("request-timeout"),
166
+ request_cert=resolve(data.get("request-cert")),
167
+ request_cert_key=resolve(data.get("request-cert-key")),
168
+ parameters=load_parameters(data),
169
+ auth=AuthConfig.from_dict(data.get("auth", {})),
170
+ warnings=WarningsConfig.from_value(data.get("warnings")),
171
+ checks=ChecksConfig.from_dict(data.get("checks", {})),
172
+ phases=PhasesConfig.from_dict(data.get("phases", {})),
173
+ generation=GenerationConfig.from_dict(data.get("generation", {})),
174
+ operations=OperationsConfig(
175
+ operations=[OperationConfig.from_dict(operation) for operation in data.get("operations", [])]
176
+ ),
177
+ )
178
+
179
+ def update(
180
+ self,
181
+ *,
182
+ base_url: str | None = None,
183
+ headers: dict | None = None,
184
+ basic_auth: tuple[str, str] | None = None,
185
+ workers: int | Literal["auto"] | None = None,
186
+ continue_on_failure: bool | None = None,
187
+ rate_limit: str | None = None,
188
+ max_redirects: int | None = None,
189
+ request_timeout: float | int | None = None,
190
+ tls_verify: bool | str | None = None,
191
+ request_cert: str | None = None,
192
+ request_cert_key: str | None = None,
193
+ parameters: dict[str, Any] | None = None,
194
+ proxy: str | None = None,
195
+ suppress_health_check: list[HealthCheck] | None = None,
196
+ warnings: WarningsConfig | None = None,
197
+ ) -> None:
198
+ if base_url is not None:
199
+ _validate_base_url(base_url)
200
+ self.base_url = base_url
201
+
202
+ if headers is not None:
203
+ _headers = self.headers or {}
204
+ _headers.update(headers)
205
+ self.headers = _headers
206
+
207
+ if basic_auth is not None:
208
+ self.auth.update(basic=basic_auth)
209
+
210
+ if workers is not None:
211
+ if isinstance(workers, int):
212
+ self.workers = workers
213
+ else:
214
+ self.workers = get_workers_count()
215
+
216
+ if continue_on_failure is not None:
217
+ self.continue_on_failure = continue_on_failure
218
+
219
+ if rate_limit is not None:
220
+ self.rate_limit = build_limiter(rate_limit)
221
+
222
+ if max_redirects is not None:
223
+ self.max_redirects = max_redirects
224
+
225
+ if request_timeout is not None:
226
+ self.request_timeout = request_timeout
227
+
228
+ if tls_verify is not None:
229
+ self.tls_verify = tls_verify
230
+
231
+ if request_cert is not None:
232
+ self.request_cert = request_cert
233
+
234
+ if request_cert_key is not None:
235
+ self.request_cert_key = request_cert_key
236
+
237
+ if proxy is not None:
238
+ self.proxy = proxy
239
+
240
+ if parameters is not None:
241
+ self.parameters = parameters
242
+
243
+ if suppress_health_check is not None:
244
+ self.suppress_health_check = suppress_health_check
245
+
246
+ if warnings is not None:
247
+ self.warnings = warnings
248
+
249
+ def auth_for(self, *, operation: APIOperation | None = None) -> tuple[str, str] | None:
250
+ """Get auth credentials, prioritizing operation-specific configs."""
251
+ if operation is not None:
252
+ config = self.operations.get_for_operation(operation=operation)
253
+ if config.auth.basic is not None:
254
+ return config.auth.basic
255
+ if self.auth.basic is not None:
256
+ return self.auth.basic
257
+ return None
258
+
259
+ def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str]:
260
+ """Get explicitly configured headers."""
261
+ headers = self.headers.copy() if self.headers else {}
262
+ if operation is not None:
263
+ config = self.operations.get_for_operation(operation=operation)
264
+ if config.headers is not None:
265
+ headers.update(config.headers)
266
+ return headers
267
+
268
+ def max_redirects_for(self, *, operation: APIOperation | None = None) -> int | None:
269
+ if operation is not None:
270
+ config = self.operations.get_for_operation(operation=operation)
271
+ if config.max_redirects is not None:
272
+ return config.max_redirects
273
+ if self.max_redirects is not None:
274
+ return self.max_redirects
275
+ return None
276
+
277
+ def request_timeout_for(self, *, operation: APIOperation | None = None) -> float | int | None:
278
+ if operation is not None:
279
+ config = self.operations.get_for_operation(operation=operation)
280
+ if config.request_timeout is not None:
281
+ return config.request_timeout
282
+ if self.request_timeout is not None:
283
+ return self.request_timeout
284
+ return None
285
+
286
+ def tls_verify_for(self, *, operation: APIOperation | None = None) -> bool | str | None:
287
+ if operation is not None:
288
+ config = self.operations.get_for_operation(operation=operation)
289
+ if config.tls_verify is not None:
290
+ return config.tls_verify
291
+ if self.tls_verify is not None:
292
+ return self.tls_verify
293
+ return None
294
+
295
+ def request_cert_for(self, *, operation: APIOperation | None = None) -> str | tuple[str, str] | None:
296
+ if operation is not None:
297
+ config = self.operations.get_for_operation(operation=operation)
298
+ if config.request_cert is not None:
299
+ if config.request_cert_key:
300
+ return (config.request_cert, config.request_cert_key)
301
+ return config.request_cert
302
+ if self.request_cert is not None:
303
+ if self.request_cert_key:
304
+ return (self.request_cert, self.request_cert_key)
305
+ return self.request_cert
306
+ return None
307
+
308
+ def proxy_for(self, *, operation: APIOperation | None = None) -> str | None:
309
+ if operation is not None:
310
+ config = self.operations.get_for_operation(operation=operation)
311
+ if config.proxy is not None:
312
+ return config.proxy
313
+ if self.proxy is not None:
314
+ return self.proxy
315
+ return None
316
+
317
+ def rate_limit_for(self, *, operation: APIOperation | None = None) -> Limiter | None:
318
+ if operation is not None:
319
+ config = self.operations.get_for_operation(operation=operation)
320
+ if config.rate_limit is not None:
321
+ return config.rate_limit
322
+ if self.rate_limit is not None:
323
+ return self.rate_limit
324
+ return None
325
+
326
+ def warnings_for(self, *, operation: APIOperation | None = None) -> WarningsConfig:
327
+ # Operation can be absent on some non-fatal errors due to schema parsing
328
+ if operation is not None:
329
+ config = self.operations.get_for_operation(operation=operation)
330
+ if config.warnings is not None:
331
+ return config.warnings
332
+ return self.warnings
333
+
334
+ def phases_for(self, *, operation: APIOperation | None) -> PhasesConfig:
335
+ configs = []
336
+ if operation is not None:
337
+ for op in self.operations.operations:
338
+ if op._filter_set.applies_to(operation=operation):
339
+ configs.append(op.phases)
340
+ if not configs:
341
+ return self.phases
342
+ configs.append(self.phases)
343
+ return PhasesConfig.from_hierarchy(configs)
344
+
345
+ def generation_for(
346
+ self,
347
+ *,
348
+ operation: APIOperation | None = None,
349
+ phase: str | None = None,
350
+ ) -> GenerationConfig:
351
+ configs = []
352
+ if operation is not None:
353
+ for op in self.operations.operations:
354
+ if op._filter_set.applies_to(operation=operation):
355
+ if phase is not None:
356
+ phase_config = op.phases.get_by_name(name=phase)
357
+ configs.append(phase_config.generation)
358
+ configs.append(op.generation)
359
+ if phase is not None:
360
+ phases = self.phases_for(operation=operation)
361
+ phase_config = phases.get_by_name(name=phase)
362
+ if not phase_config._is_default:
363
+ configs.append(phase_config.generation)
364
+ if not configs:
365
+ return self.generation
366
+ configs.append(self.generation)
367
+ return GenerationConfig.from_hierarchy(configs)
368
+
369
+ def checks_config_for(
370
+ self,
371
+ *,
372
+ operation: APIOperation | None = None,
373
+ phase: str | None = None,
374
+ ) -> ChecksConfig:
375
+ configs = []
376
+ if operation is not None:
377
+ for op in self.operations.operations:
378
+ if op._filter_set.applies_to(operation=operation):
379
+ if phase is not None:
380
+ phase_config = op.phases.get_by_name(name=phase)
381
+ configs.append(phase_config.checks)
382
+ configs.append(op.checks)
383
+ if phase is not None:
384
+ phases = self.phases_for(operation=operation)
385
+ phase_config = phases.get_by_name(name=phase)
386
+ if not phase_config._is_default:
387
+ configs.append(phase_config.checks)
388
+ if not configs:
389
+ return self.checks
390
+ configs.append(self.checks)
391
+ return ChecksConfig.from_hierarchy(configs)
392
+
393
+ def get_hypothesis_settings(
394
+ self,
395
+ *,
396
+ operation: APIOperation | None = None,
397
+ phase: str | None = None,
398
+ ) -> hypothesis.settings:
399
+ import hypothesis
400
+ from hypothesis.database import DirectoryBasedExampleDatabase, InMemoryExampleDatabase
401
+
402
+ config = self.generation_for(operation=operation, phase=phase)
403
+ kwargs: dict[str, Any] = {}
404
+
405
+ if config.max_examples is not None:
406
+ kwargs["max_examples"] = config.max_examples
407
+ phases = set(hypothesis.Phase) - {hypothesis.Phase.explain}
408
+ if config.no_shrink:
409
+ phases.discard(hypothesis.Phase.shrink)
410
+ database = config.database
411
+ if database is not None:
412
+ if database.lower() == "none":
413
+ kwargs["database"] = None
414
+ phases.discard(hypothesis.Phase.reuse)
415
+ elif database == HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER:
416
+ kwargs["database"] = InMemoryExampleDatabase()
417
+ else:
418
+ kwargs["database"] = DirectoryBasedExampleDatabase(database)
419
+
420
+ return hypothesis.settings(
421
+ derandomize=config.deterministic,
422
+ print_blob=False,
423
+ deadline=None,
424
+ verbosity=hypothesis.Verbosity.quiet,
425
+ suppress_health_check=[check for item in self.suppress_health_check for check in item.as_hypothesis()],
426
+ phases=phases,
427
+ # NOTE: Ignoring any operation-specific config as stateful tests are not operation-specific
428
+ stateful_step_count=self.phases.stateful.max_steps,
429
+ **kwargs,
430
+ )
431
+
432
+ def _get_parent(self) -> SchemathesisConfig:
433
+ if self._parent is None:
434
+ from schemathesis.config import SchemathesisConfig
435
+
436
+ self._parent = SchemathesisConfig.discover()
437
+ return self._parent
438
+
439
+ @property
440
+ def output(self) -> OutputConfig:
441
+ return self._get_parent().output
442
+
443
+ @property
444
+ def wait_for_schema(self) -> float | int | None:
445
+ return self._get_parent().wait_for_schema
446
+
447
+ @property
448
+ def max_failures(self) -> int | None:
449
+ return self._get_parent().max_failures
450
+
451
+ @max_failures.setter
452
+ def max_failures(self, value: int) -> None:
453
+ parent = self._get_parent()
454
+ parent.max_failures = value
455
+
456
+ @property
457
+ def reports(self) -> ReportsConfig:
458
+ return self._get_parent().reports
459
+
460
+ @property
461
+ def suppress_health_check(self) -> list[HealthCheck]:
462
+ return self._get_parent().suppress_health_check
463
+
464
+ @suppress_health_check.setter
465
+ def suppress_health_check(self, value: list[HealthCheck]) -> None:
466
+ parent = self._get_parent()
467
+ parent.suppress_health_check = value
468
+
469
+ @property
470
+ def seed(self) -> int:
471
+ return self._get_parent().seed
472
+
473
+ @seed.setter
474
+ def seed(self, value: int) -> None:
475
+ parent = self._get_parent()
476
+ parent._seed = value
477
+
478
+
479
+ def _validate_base_url(base_url: str) -> None:
480
+ try:
481
+ validate_base_url(base_url)
482
+ except ValueError as exc:
483
+ raise ConfigError(str(exc)) from None
484
+
485
+
486
+ @dataclass(repr=False)
487
+ class ProjectsConfig(DiffBase):
488
+ default: ProjectConfig
489
+ named: dict[str, ProjectConfig]
490
+ _override: ProjectConfig
491
+
492
+ __slots__ = ("default", "named", "_override")
493
+
494
+ def __init__(
495
+ self,
496
+ *,
497
+ default: ProjectConfig | None = None,
498
+ named: dict[str, ProjectConfig] | None = None,
499
+ ) -> None:
500
+ self.default = default or ProjectConfig()
501
+ self.named = named or {}
502
+
503
+ @property
504
+ def override(self) -> ProjectConfig:
505
+ if not hasattr(self, "_override"):
506
+ self._override = ProjectConfig()
507
+ self._override._parent = self.default._parent
508
+ return self._override
509
+
510
+ @classmethod
511
+ def from_dict(cls, data: dict[str, Any]) -> ProjectsConfig:
512
+ return cls(
513
+ default=ProjectConfig.from_dict(data),
514
+ named={project["title"]: ProjectConfig.from_dict(project) for project in data.get("project", [])},
515
+ )
516
+
517
+ def _set_parent(self, parent: SchemathesisConfig) -> None:
518
+ self.default._parent = parent
519
+ for project in self.named.values():
520
+ project._parent = parent
521
+
522
+ def get_default(self) -> ProjectConfig:
523
+ config = ProjectConfig.from_hierarchy([self.override, self.default])
524
+ config._parent = self.default._parent
525
+ return config
526
+
527
+ def get(self, schema: dict[str, Any]) -> ProjectConfig:
528
+ # Highest priority goes to `override`, then config specifically
529
+ # for the given project, then the "default" project config
530
+ configs = []
531
+ if hasattr(self, "_override"):
532
+ configs.append(self._override)
533
+ title = schema.get("info", {}).get("title")
534
+ if title is not None:
535
+ named = self.named.get(title)
536
+ if named is not None:
537
+ configs.append(named)
538
+ if not configs:
539
+ return self.default
540
+ configs.append(self.default)
541
+ config = ProjectConfig.from_hierarchy(configs)
542
+ config._parent = self.default._parent
543
+ 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 | None = None,
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)