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,189 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from os import PathLike
6
+ from random import Random
7
+
8
+ import tomli
9
+
10
+ from schemathesis.config._checks import (
11
+ CheckConfig,
12
+ ChecksConfig,
13
+ NotAServerErrorConfig,
14
+ PositiveDataAcceptanceConfig,
15
+ SimpleCheckConfig,
16
+ )
17
+ from schemathesis.config._diff_base import DiffBase
18
+ from schemathesis.config._error import ConfigError
19
+ from schemathesis.config._generation import GenerationConfig
20
+ from schemathesis.config._health_check import HealthCheck
21
+ from schemathesis.config._output import OutputConfig, SanitizationConfig, TruncationConfig
22
+ from schemathesis.config._phases import CoveragePhaseConfig, PhaseConfig, PhasesConfig, StatefulPhaseConfig
23
+ from schemathesis.config._projects import ProjectConfig, ProjectsConfig, SchemathesisWarning, get_workers_count
24
+ from schemathesis.config._report import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat, ReportsConfig
25
+
26
+ __all__ = [
27
+ "SchemathesisConfig",
28
+ "ConfigError",
29
+ "HealthCheck",
30
+ "ReportConfig",
31
+ "ReportsConfig",
32
+ "ReportFormat",
33
+ "DEFAULT_REPORT_DIRECTORY",
34
+ "GenerationConfig",
35
+ "OutputConfig",
36
+ "SanitizationConfig",
37
+ "TruncationConfig",
38
+ "ChecksConfig",
39
+ "CheckConfig",
40
+ "NotAServerErrorConfig",
41
+ "PositiveDataAcceptanceConfig",
42
+ "SimpleCheckConfig",
43
+ "PhaseConfig",
44
+ "PhasesConfig",
45
+ "CoveragePhaseConfig",
46
+ "StatefulPhaseConfig",
47
+ "ProjectsConfig",
48
+ "ProjectConfig",
49
+ "get_workers_count",
50
+ "SchemathesisWarning",
51
+ ]
52
+
53
+
54
+ @dataclass(repr=False)
55
+ class SchemathesisConfig(DiffBase):
56
+ color: bool | None
57
+ suppress_health_check: list[HealthCheck]
58
+ _seed: int | None
59
+ wait_for_schema: float | int | None
60
+ max_failures: int | None
61
+ reports: ReportsConfig
62
+ output: OutputConfig
63
+ projects: ProjectsConfig
64
+
65
+ __slots__ = (
66
+ "color",
67
+ "suppress_health_check",
68
+ "_seed",
69
+ "wait_for_schema",
70
+ "max_failures",
71
+ "reports",
72
+ "output",
73
+ "projects",
74
+ )
75
+
76
+ def __init__(
77
+ self,
78
+ *,
79
+ color: bool | None = None,
80
+ suppress_health_check: list[HealthCheck] | None = None,
81
+ seed: int | None = None,
82
+ wait_for_schema: float | int | None = None,
83
+ max_failures: int | None = None,
84
+ reports: ReportsConfig | None = None,
85
+ output: OutputConfig | None = None,
86
+ projects: ProjectsConfig | None = None,
87
+ ):
88
+ self.color = color
89
+ self.suppress_health_check = suppress_health_check or []
90
+ self._seed = seed
91
+ self.wait_for_schema = wait_for_schema
92
+ self.max_failures = max_failures
93
+ self.reports = reports or ReportsConfig()
94
+ self.output = output or OutputConfig()
95
+ self.projects = projects or ProjectsConfig()
96
+ self.projects._set_parent(self)
97
+
98
+ @property
99
+ def seed(self) -> int:
100
+ if self._seed is None:
101
+ self._seed = Random().getrandbits(128)
102
+ return self._seed
103
+
104
+ @classmethod
105
+ def discover(cls) -> SchemathesisConfig:
106
+ """Discover the Schemathesis configuration file.
107
+
108
+ Search for 'schemathesis.toml' in the current directory and then in each parent directory,
109
+ stopping when a directory containing a '.git' folder is encountered or the filesystem root is reached.
110
+ If a config file is found, load it; otherwise, return a default configuration.
111
+ """
112
+ current_dir = os.getcwd()
113
+ config_file = None
114
+
115
+ while True:
116
+ candidate = os.path.join(current_dir, "schemathesis.toml")
117
+ if os.path.isfile(candidate):
118
+ config_file = candidate
119
+ break
120
+
121
+ # Stop searching if we've reached a git repository root
122
+ git_dir = os.path.join(current_dir, ".git")
123
+ if os.path.isdir(git_dir):
124
+ break
125
+
126
+ # Stop if we've reached the filesystem root
127
+ parent = os.path.dirname(current_dir)
128
+ if parent == current_dir:
129
+ break
130
+ current_dir = parent
131
+
132
+ if config_file:
133
+ return cls.from_path(config_file)
134
+ return cls()
135
+
136
+ def update(
137
+ self,
138
+ *,
139
+ color: bool | None = None,
140
+ suppress_health_check: list[HealthCheck] | None = None,
141
+ seed: int | None = None,
142
+ wait_for_schema: float | int | None = None,
143
+ max_failures: int | None,
144
+ ) -> None:
145
+ """Set top-level configuration options."""
146
+ if color is not None:
147
+ self.color = color
148
+ if suppress_health_check is not None:
149
+ self.suppress_health_check = suppress_health_check
150
+ if seed is not None:
151
+ self._seed = seed
152
+ if wait_for_schema is not None:
153
+ self.wait_for_schema = wait_for_schema
154
+ if max_failures is not None:
155
+ self.max_failures = max_failures
156
+
157
+ @classmethod
158
+ def from_path(cls, path: PathLike | str) -> SchemathesisConfig:
159
+ """Load configuration from a file path."""
160
+ with open(path, encoding="utf-8") as fd:
161
+ return cls.from_str(fd.read())
162
+
163
+ @classmethod
164
+ def from_str(cls, data: str) -> SchemathesisConfig:
165
+ """Parse configuration from a string."""
166
+ parsed = tomli.loads(data)
167
+ return cls.from_dict(parsed)
168
+
169
+ @classmethod
170
+ def from_dict(cls, data: dict) -> SchemathesisConfig:
171
+ """Create a config instance from a dictionary."""
172
+ from jsonschema.exceptions import ValidationError
173
+
174
+ from schemathesis.config._validator import CONFIG_VALIDATOR
175
+
176
+ try:
177
+ CONFIG_VALIDATOR.validate(data)
178
+ except ValidationError as exc:
179
+ raise ConfigError.from_validation_error(exc) from None
180
+ return cls(
181
+ color=data.get("color"),
182
+ suppress_health_check=[HealthCheck(name) for name in data.get("suppress-health-check", [])],
183
+ seed=data.get("seed"),
184
+ wait_for_schema=data.get("wait-for-schema"),
185
+ max_failures=data.get("max-failures"),
186
+ reports=ReportsConfig.from_dict(data.get("reports", {})),
187
+ output=OutputConfig.from_dict(data.get("output", {})),
188
+ projects=ProjectsConfig.from_dict(data),
189
+ )
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from schemathesis.config._diff_base import DiffBase
7
+ from schemathesis.config._env import resolve
8
+ from schemathesis.config._error import ConfigError
9
+ from schemathesis.core.validation import is_latin_1_encodable
10
+
11
+
12
+ @dataclass(repr=False)
13
+ class AuthConfig(DiffBase):
14
+ basic: tuple[str, str] | None
15
+
16
+ __slots__ = ("basic",)
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ basic: dict[str, str] | None = None,
22
+ ) -> None:
23
+ if basic is not None:
24
+ assert "username" in basic
25
+ username = resolve(basic["username"])
26
+ assert "password" in basic
27
+ password = resolve(basic["password"])
28
+ _validate_basic(username, password)
29
+ self.basic = (username, password)
30
+ else:
31
+ self.basic = None
32
+
33
+ def update(self, *, basic: tuple[str, str] | None = None) -> None:
34
+ if basic is not None:
35
+ _validate_basic(*basic)
36
+ self.basic = basic
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: dict[str, Any]) -> AuthConfig:
40
+ return cls(basic=data.get("basic"))
41
+
42
+ @property
43
+ def is_defined(self) -> bool:
44
+ return self.basic is not None
45
+
46
+
47
+ def _validate_basic(username: str, password: str) -> None:
48
+ if not is_latin_1_encodable(username):
49
+ raise ConfigError("Username should be latin-1 encodable.")
50
+ if not is_latin_1_encodable(password):
51
+ raise ConfigError("Password should be latin-1 encodable.")
@@ -0,0 +1,268 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, ClassVar, Sequence
5
+
6
+ from schemathesis.config._diff_base import DiffBase
7
+ from schemathesis.config._error import ConfigError
8
+
9
+ if TYPE_CHECKING:
10
+ from typing_extensions import Self
11
+
12
+ NOT_A_SERVER_ERROR_EXPECTED_STATUSES = ["2xx", "3xx", "4xx"]
13
+ NEGATIVE_DATA_REJECTION_EXPECTED_STATUSES = ["400", "401", "403", "404", "406", "422", "428", "5xx"]
14
+ POSITIVE_DATA_ACCEPTANCE_EXPECTED_STATUSES = ["2xx", "401", "403", "404", "5xx"]
15
+ MISSING_REQUIRED_HEADER_EXPECTED_STATUSES = ["406"]
16
+
17
+
18
+ def validate_status_codes(value: Sequence[str] | None) -> Sequence[str] | None:
19
+ if not value:
20
+ return value
21
+
22
+ invalid = []
23
+
24
+ for code in value:
25
+ if len(code) != 3:
26
+ invalid.append(code)
27
+ continue
28
+
29
+ if code[0] not in {"1", "2", "3", "4", "5"}:
30
+ invalid.append(code)
31
+ continue
32
+
33
+ upper_code = code.upper()
34
+
35
+ if "X" in upper_code:
36
+ if (
37
+ upper_code[1:] == "XX"
38
+ or (upper_code[1] == "X" and upper_code[2].isdigit())
39
+ or (upper_code[1].isdigit() and upper_code[2] == "X")
40
+ ):
41
+ continue
42
+ else:
43
+ invalid.append(code)
44
+ continue
45
+
46
+ if not code.isnumeric():
47
+ invalid.append(code)
48
+
49
+ if invalid:
50
+ raise ConfigError(
51
+ f"Invalid status code(s): {', '.join(invalid)}. "
52
+ "Use valid 3-digit codes between 100 and 599, "
53
+ "or wildcards (e.g., 2XX, 2X0, 20X), where X is a wildcard digit."
54
+ )
55
+ return value
56
+
57
+
58
+ @dataclass(repr=False)
59
+ class SimpleCheckConfig(DiffBase):
60
+ enabled: bool
61
+
62
+ __slots__ = ("enabled",)
63
+
64
+ def __init__(self, *, enabled: bool = True) -> None:
65
+ self.enabled = enabled
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: dict[str, Any]) -> SimpleCheckConfig:
69
+ return cls(enabled=data.get("enabled", True))
70
+
71
+
72
+ @dataclass(repr=False)
73
+ class MaxResponseTimeConfig(DiffBase):
74
+ enabled: bool
75
+ limit: float | None
76
+
77
+ __slots__ = ("enabled", "limit")
78
+
79
+ def __init__(self, *, limit: float | None = None) -> None:
80
+ self.enabled = limit is not None
81
+ self.limit = limit
82
+
83
+
84
+ @dataclass(repr=False)
85
+ class CheckConfig(DiffBase):
86
+ enabled: bool
87
+ expected_statuses: list[str]
88
+ _DEFAULT_EXPECTED_STATUSES: ClassVar[list[str]]
89
+
90
+ __slots__ = ("enabled", "expected_statuses")
91
+
92
+ def __init__(self, *, enabled: bool = True, expected_statuses: Sequence[str | int] | None = None) -> None:
93
+ self.enabled = enabled
94
+ if expected_statuses is not None:
95
+ statuses = [str(status) for status in expected_statuses]
96
+ validate_status_codes(statuses)
97
+ self.expected_statuses = statuses
98
+ else:
99
+ self.expected_statuses = self._DEFAULT_EXPECTED_STATUSES
100
+
101
+ @classmethod
102
+ def from_dict(cls, data: dict[str, Any]) -> Self:
103
+ enabled = data.get("enabled", True)
104
+ return cls(
105
+ enabled=enabled,
106
+ expected_statuses=data.get("expected-statuses", cls._DEFAULT_EXPECTED_STATUSES),
107
+ )
108
+
109
+
110
+ class NotAServerErrorConfig(CheckConfig):
111
+ _DEFAULT_EXPECTED_STATUSES = NOT_A_SERVER_ERROR_EXPECTED_STATUSES
112
+
113
+
114
+ class PositiveDataAcceptanceConfig(CheckConfig):
115
+ _DEFAULT_EXPECTED_STATUSES = POSITIVE_DATA_ACCEPTANCE_EXPECTED_STATUSES
116
+
117
+
118
+ class NegativeDataRejectionConfig(CheckConfig):
119
+ _DEFAULT_EXPECTED_STATUSES = NEGATIVE_DATA_REJECTION_EXPECTED_STATUSES
120
+
121
+
122
+ class MissingRequiredHeaderConfig(CheckConfig):
123
+ _DEFAULT_EXPECTED_STATUSES = MISSING_REQUIRED_HEADER_EXPECTED_STATUSES
124
+
125
+
126
+ @dataclass(repr=False)
127
+ class ChecksConfig(DiffBase):
128
+ not_a_server_error: NotAServerErrorConfig
129
+ status_code_conformance: SimpleCheckConfig
130
+ content_type_conformance: SimpleCheckConfig
131
+ response_schema_conformance: SimpleCheckConfig
132
+ response_headers_conformance: SimpleCheckConfig
133
+ positive_data_acceptance: PositiveDataAcceptanceConfig
134
+ negative_data_rejection: NegativeDataRejectionConfig
135
+ use_after_free: SimpleCheckConfig
136
+ ensure_resource_availability: SimpleCheckConfig
137
+ missing_required_header: MissingRequiredHeaderConfig
138
+ ignored_auth: SimpleCheckConfig
139
+ unsupported_method: SimpleCheckConfig
140
+ max_response_time: MaxResponseTimeConfig
141
+ _unknown: dict[str, SimpleCheckConfig]
142
+
143
+ __slots__ = (
144
+ "not_a_server_error",
145
+ "status_code_conformance",
146
+ "content_type_conformance",
147
+ "response_schema_conformance",
148
+ "response_headers_conformance",
149
+ "positive_data_acceptance",
150
+ "negative_data_rejection",
151
+ "use_after_free",
152
+ "ensure_resource_availability",
153
+ "missing_required_header",
154
+ "ignored_auth",
155
+ "unsupported_method",
156
+ "max_response_time",
157
+ "_unknown",
158
+ )
159
+
160
+ def __init__(
161
+ self,
162
+ *,
163
+ not_a_server_error: NotAServerErrorConfig | None = None,
164
+ status_code_conformance: SimpleCheckConfig | None = None,
165
+ content_type_conformance: SimpleCheckConfig | None = None,
166
+ response_schema_conformance: SimpleCheckConfig | None = None,
167
+ response_headers_conformance: SimpleCheckConfig | None = None,
168
+ positive_data_acceptance: PositiveDataAcceptanceConfig | None = None,
169
+ negative_data_rejection: NegativeDataRejectionConfig | None = None,
170
+ use_after_free: SimpleCheckConfig | None = None,
171
+ ensure_resource_availability: SimpleCheckConfig | None = None,
172
+ missing_required_header: MissingRequiredHeaderConfig | None = None,
173
+ ignored_auth: SimpleCheckConfig | None = None,
174
+ unsupported_method: SimpleCheckConfig | None = None,
175
+ max_response_time: MaxResponseTimeConfig | None = None,
176
+ ) -> None:
177
+ self.not_a_server_error = not_a_server_error or NotAServerErrorConfig()
178
+ self.status_code_conformance = status_code_conformance or SimpleCheckConfig()
179
+ self.content_type_conformance = content_type_conformance or SimpleCheckConfig()
180
+ self.response_schema_conformance = response_schema_conformance or SimpleCheckConfig()
181
+ self.response_headers_conformance = response_headers_conformance or SimpleCheckConfig()
182
+ self.positive_data_acceptance = positive_data_acceptance or PositiveDataAcceptanceConfig()
183
+ self.negative_data_rejection = negative_data_rejection or NegativeDataRejectionConfig()
184
+ self.use_after_free = use_after_free or SimpleCheckConfig()
185
+ self.ensure_resource_availability = ensure_resource_availability or SimpleCheckConfig()
186
+ self.missing_required_header = missing_required_header or MissingRequiredHeaderConfig()
187
+ self.ignored_auth = ignored_auth or SimpleCheckConfig()
188
+ self.unsupported_method = unsupported_method or SimpleCheckConfig()
189
+ self.max_response_time = max_response_time or MaxResponseTimeConfig()
190
+ self._unknown = {}
191
+
192
+ @classmethod
193
+ def from_dict(cls, data: dict[str, Any]) -> ChecksConfig:
194
+ # Use the outer "enabled" value as default for all checks.
195
+ default_enabled = data.get("enabled", None)
196
+
197
+ def merge(sub: dict[str, Any]) -> dict[str, Any]:
198
+ # Merge the default enabled flag with the sub-dict; the sub-dict takes precedence.
199
+ if default_enabled is not None:
200
+ return {"enabled": default_enabled, **sub}
201
+ return sub
202
+
203
+ return cls(
204
+ not_a_server_error=NotAServerErrorConfig.from_dict(
205
+ merge(data.get("not_a_server_error", {})),
206
+ ),
207
+ status_code_conformance=SimpleCheckConfig.from_dict(merge(data.get("status_code_conformance", {}))),
208
+ content_type_conformance=SimpleCheckConfig.from_dict(merge(data.get("content_type_conformance", {}))),
209
+ response_schema_conformance=SimpleCheckConfig.from_dict(merge(data.get("response_schema_conformance", {}))),
210
+ response_headers_conformance=SimpleCheckConfig.from_dict(
211
+ merge(data.get("response_headers_conformance", {}))
212
+ ),
213
+ positive_data_acceptance=PositiveDataAcceptanceConfig.from_dict(
214
+ merge(data.get("positive_data_acceptance", {})),
215
+ ),
216
+ negative_data_rejection=NegativeDataRejectionConfig.from_dict(
217
+ merge(data.get("negative_data_rejection", {})),
218
+ ),
219
+ use_after_free=SimpleCheckConfig.from_dict(merge(data.get("use_after_free", {}))),
220
+ ensure_resource_availability=SimpleCheckConfig.from_dict(
221
+ merge(data.get("ensure_resource_availability", {}))
222
+ ),
223
+ missing_required_header=MissingRequiredHeaderConfig.from_dict(
224
+ merge(data.get("missing_required_header", {})),
225
+ ),
226
+ ignored_auth=SimpleCheckConfig.from_dict(merge(data.get("ignored_auth", {}))),
227
+ unsupported_method=SimpleCheckConfig.from_dict(merge(data.get("unsupported_method", {}))),
228
+ max_response_time=MaxResponseTimeConfig(limit=data.get("max_response_time")),
229
+ )
230
+
231
+ def get_by_name(self, *, name: str) -> CheckConfig | SimpleCheckConfig | MaxResponseTimeConfig:
232
+ try:
233
+ return getattr(self, name)
234
+ except AttributeError:
235
+ return self._unknown.setdefault(name, SimpleCheckConfig())
236
+
237
+ def update(
238
+ self,
239
+ *,
240
+ included_check_names: list[str] | None = None,
241
+ excluded_check_names: list[str] | None = None,
242
+ max_response_time: float | None = None,
243
+ ) -> None:
244
+ known_names = {name for name in self.__slots__ if not name.startswith("_")}
245
+ for name in known_names:
246
+ # Check in explicitly excluded or not in explicitly included
247
+ if name in (excluded_check_names or []) or (
248
+ included_check_names is not None
249
+ and "all" not in included_check_names
250
+ and name not in included_check_names
251
+ ):
252
+ config = self.get_by_name(name=name)
253
+ config.enabled = False
254
+ elif included_check_names is not None and name in included_check_names:
255
+ config = self.get_by_name(name=name)
256
+ config.enabled = True
257
+
258
+ if max_response_time is not None:
259
+ self.max_response_time.enabled = True
260
+ self.max_response_time.limit = max_response_time
261
+
262
+ for name in included_check_names or []:
263
+ if name not in known_names and name != "all":
264
+ self._unknown[name] = SimpleCheckConfig(enabled=True)
265
+
266
+ for name in excluded_check_names or []:
267
+ if name not in known_names and name != "all":
268
+ self._unknown[name] = SimpleCheckConfig(enabled=False)
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, fields, is_dataclass
4
+ from typing import TypeVar
5
+
6
+ T = TypeVar("T", bound="DiffBase")
7
+
8
+
9
+ @dataclass
10
+ class DiffBase:
11
+ def __repr__(self) -> str:
12
+ """Show only the fields that differ from the default."""
13
+ assert is_dataclass(self)
14
+ default = self.__class__()
15
+ diffs = []
16
+ for field in fields(self):
17
+ name = field.name
18
+ if name.startswith("_") and name not in ("_seed", "_filter_set"):
19
+ continue
20
+ current_value = getattr(self, name)
21
+ default_value = getattr(default, name)
22
+ if name == "_seed":
23
+ name = "seed"
24
+ if name == "_filter_set":
25
+ name = "filter_set"
26
+ if name == "rate_limit" and current_value is not None:
27
+ assert hasattr(self, "_rate_limit")
28
+ current_value = self._rate_limit
29
+ if self._has_diff(current_value, default_value):
30
+ diffs.append(f"{name}={self._diff_repr(current_value, default_value)}")
31
+ return f"{self.__class__.__name__}({', '.join(diffs)})"
32
+
33
+ def _has_diff(self, value: object, default: object) -> bool:
34
+ if is_dataclass(value):
35
+ return repr(value) != repr(default)
36
+ if isinstance(value, list) and isinstance(default, list):
37
+ if len(value) != len(default):
38
+ return True
39
+ return any(self._has_diff(v, d) for v, d in zip(value, default))
40
+ if isinstance(value, dict) and isinstance(default, dict):
41
+ if set(value.keys()) != set(default.keys()):
42
+ return True
43
+ return any(self._has_diff(value[k], default[k]) for k in value)
44
+ return value != default
45
+
46
+ def _diff_repr(self, value: object, default: object) -> str:
47
+ if is_dataclass(value):
48
+ # If the nested object is a dataclass, recursively show its diff.
49
+ return repr(value)
50
+ if isinstance(value, list) and isinstance(default, list):
51
+ diff_items = []
52
+ # Compare items pairwise.
53
+ for v, d in zip(value, default):
54
+ if self._has_diff(v, d):
55
+ diff_items.append(self._diff_repr(v, d))
56
+ # Include any extra items in value.
57
+ if len(value) > len(default):
58
+ diff_items.extend(_repr(item) for item in value[len(default) :])
59
+ return f"[{', '.join(_repr(item) for item in value)}]"
60
+ if isinstance(value, dict) and isinstance(default, dict):
61
+ diff_items = []
62
+ for k, v in value.items():
63
+ d = default.get(k)
64
+ if self._has_diff(v, d):
65
+ diff_items.append(f"{k!r}: {self._diff_repr(v, d)}")
66
+ return f"{{{', '.join(diff_items)}}}"
67
+ return repr(value)
68
+
69
+ @classmethod
70
+ def from_hierarchy(cls, configs: list[T]) -> T:
71
+ # This config will accumulate "merged" config options
72
+ output = cls()
73
+ for option in cls.__slots__: # type: ignore
74
+ if option.startswith("_"):
75
+ continue
76
+ default = getattr(output, option)
77
+ if is_dataclass(default):
78
+ # Sub-configs require merging of nested config options
79
+ sub_configs = [getattr(config, option) for config in configs]
80
+ merged = type(default).from_hierarchy(sub_configs) # type: ignore[union-attr]
81
+ setattr(output, option, merged)
82
+ else:
83
+ # Primitive config options can be compared directly and do not
84
+ # require merging of nested options
85
+ for config in configs:
86
+ current = getattr(config, option)
87
+ if current != default:
88
+ setattr(output, option, current)
89
+ # As we go from the highest priority to the lowest one,
90
+ # we can just stop on the first non-default value
91
+ break
92
+ return output # type: ignore
93
+
94
+
95
+ def _repr(item: object) -> str:
96
+ if callable(item) and hasattr(item, "__name__"):
97
+ return f"<function {item.__name__}>"
98
+
99
+ return repr(item)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from string import Template
5
+ from typing import Any
6
+
7
+ from schemathesis.config._error import ConfigError
8
+
9
+
10
+ def resolve(value: Any) -> Any:
11
+ """Resolve environment variables using string templates."""
12
+ if value is None:
13
+ return None
14
+ if not isinstance(value, str):
15
+ return value
16
+ try:
17
+ return Template(value).substitute(os.environ)
18
+ except ValueError:
19
+ raise ConfigError(f"Invalid placeholder in string: `{value}`") from None
20
+ except KeyError:
21
+ raise ConfigError(f"Missing environment variable: `{value}`") from None