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,81 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any, NoReturn
5
+
6
+ import click
7
+
8
+ from schemathesis.core.registries import Registry
9
+
10
+
11
+ class CustomHelpMessageChoice(click.Choice):
12
+ """Allows you to customize how choices are displayed in the help message."""
13
+
14
+ def __init__(self, *args: Any, choices_repr: str, **kwargs: Any):
15
+ super().__init__(*args, **kwargs)
16
+ self.choices_repr = choices_repr
17
+
18
+ def get_metavar(self, param: click.Parameter) -> str:
19
+ return self.choices_repr
20
+
21
+
22
+ class BaseCsvChoice(click.Choice):
23
+ def parse_value(self, value: str) -> tuple[list[str], set[str]]:
24
+ selected = [item.strip() for item in value.split(",") if item.strip()]
25
+ if not self.case_sensitive:
26
+ invalid_options = {
27
+ item for item in selected if item.upper() not in {choice.upper() for choice in self.choices}
28
+ }
29
+ else:
30
+ invalid_options = set(selected) - set(self.choices)
31
+ return selected, invalid_options
32
+
33
+ def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn: # type: ignore[misc]
34
+ # Sort to keep the error output consistent with the passed values
35
+ sorted_options = ", ".join(sorted(invalid_options, key=selected.index))
36
+ available_options = ", ".join(self.choices)
37
+ self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}.")
38
+
39
+
40
+ class CsvChoice(BaseCsvChoice):
41
+ def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
42
+ selected, invalid_options = self.parse_value(value)
43
+ if not invalid_options and selected:
44
+ return selected
45
+ self.fail_on_invalid_options(invalid_options, selected)
46
+
47
+
48
+ class CsvEnumChoice(BaseCsvChoice):
49
+ def __init__(self, choices: type[Enum], case_sensitive: bool = False):
50
+ self.enum = choices
51
+ super().__init__(tuple(el.name.lower() for el in choices), case_sensitive=case_sensitive)
52
+
53
+ def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[Enum]:
54
+ selected, invalid_options = self.parse_value(value)
55
+ if not invalid_options and selected:
56
+ # Match case-insensitively to find the correct enum
57
+ return [
58
+ next(enum_value for enum_value in self.enum if enum_value.value.upper() == item.upper())
59
+ for item in selected
60
+ ]
61
+ self.fail_on_invalid_options(invalid_options, selected)
62
+
63
+
64
+ class RegistryChoice(BaseCsvChoice):
65
+ def __init__(self, registry: Registry, with_all: bool = False) -> None:
66
+ self.registry = registry
67
+ self.case_sensitive = True
68
+ self.with_all = with_all
69
+
70
+ @property
71
+ def choices(self) -> list[str]:
72
+ choices = self.registry.get_all_names()
73
+ if self.with_all:
74
+ choices.append("all")
75
+ return choices
76
+
77
+ def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
78
+ selected, invalid_options = self.parse_value(value)
79
+ if not invalid_options and selected:
80
+ return selected
81
+ self.fail_on_invalid_options(invalid_options, selected)
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from os import PathLike
7
+ from random import Random
8
+
9
+ from schemathesis.config._checks import (
10
+ CheckConfig,
11
+ ChecksConfig,
12
+ NotAServerErrorConfig,
13
+ PositiveDataAcceptanceConfig,
14
+ SimpleCheckConfig,
15
+ )
16
+ from schemathesis.config._diff_base import DiffBase
17
+ from schemathesis.config._error import ConfigError
18
+ from schemathesis.config._generation import GenerationConfig
19
+ from schemathesis.config._health_check import HealthCheck
20
+ from schemathesis.config._output import OutputConfig, SanitizationConfig, TruncationConfig
21
+ from schemathesis.config._phases import (
22
+ CoveragePhaseConfig,
23
+ InferenceAlgorithm,
24
+ PhaseConfig,
25
+ PhasesConfig,
26
+ StatefulPhaseConfig,
27
+ )
28
+ from schemathesis.config._projects import ProjectConfig, ProjectsConfig, get_workers_count
29
+ from schemathesis.config._report import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat, ReportsConfig
30
+ from schemathesis.config._warnings import SchemathesisWarning, WarningsConfig
31
+
32
+ if sys.version_info < (3, 11):
33
+ import tomli
34
+ else:
35
+ import tomllib as tomli
36
+
37
+ __all__ = [
38
+ "SchemathesisConfig",
39
+ "ConfigError",
40
+ "HealthCheck",
41
+ "ReportConfig",
42
+ "ReportsConfig",
43
+ "ReportFormat",
44
+ "DEFAULT_REPORT_DIRECTORY",
45
+ "GenerationConfig",
46
+ "OutputConfig",
47
+ "SanitizationConfig",
48
+ "TruncationConfig",
49
+ "ChecksConfig",
50
+ "CheckConfig",
51
+ "NotAServerErrorConfig",
52
+ "PositiveDataAcceptanceConfig",
53
+ "SimpleCheckConfig",
54
+ "PhaseConfig",
55
+ "PhasesConfig",
56
+ "CoveragePhaseConfig",
57
+ "StatefulPhaseConfig",
58
+ "InferenceAlgorithm",
59
+ "ProjectsConfig",
60
+ "ProjectConfig",
61
+ "get_workers_count",
62
+ "SchemathesisWarning",
63
+ "WarningsConfig",
64
+ ]
65
+
66
+
67
+ @dataclass(repr=False)
68
+ class SchemathesisConfig(DiffBase):
69
+ color: bool | None
70
+ suppress_health_check: list[HealthCheck]
71
+ _seed: int | None
72
+ wait_for_schema: float | int | None
73
+ max_failures: int | None
74
+ reports: ReportsConfig
75
+ output: OutputConfig
76
+ projects: ProjectsConfig
77
+
78
+ __slots__ = (
79
+ "color",
80
+ "suppress_health_check",
81
+ "_seed",
82
+ "wait_for_schema",
83
+ "max_failures",
84
+ "reports",
85
+ "output",
86
+ "projects",
87
+ )
88
+
89
+ def __init__(
90
+ self,
91
+ *,
92
+ color: bool | None = None,
93
+ suppress_health_check: list[HealthCheck] | None = None,
94
+ seed: int | None = None,
95
+ wait_for_schema: float | int | None = None,
96
+ max_failures: int | None = None,
97
+ reports: ReportsConfig | None = None,
98
+ output: OutputConfig | None = None,
99
+ projects: ProjectsConfig | None = None,
100
+ ):
101
+ self.color = color
102
+ self.suppress_health_check = suppress_health_check or []
103
+ self._seed = seed
104
+ self.wait_for_schema = wait_for_schema
105
+ self.max_failures = max_failures
106
+ self.reports = reports or ReportsConfig()
107
+ self.output = output or OutputConfig()
108
+ self.projects = projects or ProjectsConfig()
109
+ self.projects._set_parent(self)
110
+
111
+ @property
112
+ def seed(self) -> int:
113
+ if self._seed is None:
114
+ self._seed = Random().getrandbits(128)
115
+ return self._seed
116
+
117
+ @classmethod
118
+ def discover(cls) -> SchemathesisConfig:
119
+ """Discover the Schemathesis configuration file.
120
+
121
+ Search for 'schemathesis.toml' in the current directory and then in each parent directory,
122
+ stopping when a directory containing a '.git' folder is encountered or the filesystem root is reached.
123
+ If a config file is found, load it; otherwise, return a default configuration.
124
+ """
125
+ current_dir = os.getcwd()
126
+ config_file = None
127
+
128
+ while True:
129
+ candidate = os.path.join(current_dir, "schemathesis.toml")
130
+ if os.path.isfile(candidate):
131
+ config_file = candidate
132
+ break
133
+
134
+ # Stop searching if we've reached a git repository root
135
+ git_dir = os.path.join(current_dir, ".git")
136
+ if os.path.isdir(git_dir):
137
+ break
138
+
139
+ # Stop if we've reached the filesystem root
140
+ parent = os.path.dirname(current_dir)
141
+ if parent == current_dir:
142
+ break
143
+ current_dir = parent
144
+
145
+ if config_file:
146
+ return cls.from_path(config_file)
147
+ return cls()
148
+
149
+ def update(
150
+ self,
151
+ *,
152
+ color: bool | None = None,
153
+ suppress_health_check: list[HealthCheck] | None = None,
154
+ seed: int | None = None,
155
+ wait_for_schema: float | int | None = None,
156
+ max_failures: int | None,
157
+ ) -> None:
158
+ """Set top-level configuration options."""
159
+ if color is not None:
160
+ self.color = color
161
+ if suppress_health_check is not None:
162
+ self.suppress_health_check = suppress_health_check
163
+ if seed is not None:
164
+ self._seed = seed
165
+ if wait_for_schema is not None:
166
+ self.wait_for_schema = wait_for_schema
167
+ if max_failures is not None:
168
+ self.max_failures = max_failures
169
+
170
+ @classmethod
171
+ def from_path(cls, path: PathLike | str) -> SchemathesisConfig:
172
+ """Load configuration from a file path."""
173
+ with open(path, encoding="utf-8") as fd:
174
+ return cls.from_str(fd.read())
175
+
176
+ @classmethod
177
+ def from_str(cls, data: str) -> SchemathesisConfig:
178
+ """Parse configuration from a string."""
179
+ parsed = tomli.loads(data)
180
+ return cls.from_dict(parsed)
181
+
182
+ @classmethod
183
+ def from_dict(cls, data: dict) -> SchemathesisConfig:
184
+ """Create a config instance from a dictionary."""
185
+ from jsonschema.exceptions import ValidationError
186
+
187
+ from schemathesis.config._validator import CONFIG_VALIDATOR
188
+
189
+ try:
190
+ CONFIG_VALIDATOR.validate(data)
191
+ except ValidationError as exc:
192
+ raise ConfigError.from_validation_error(exc) from None
193
+ return cls(
194
+ color=data.get("color"),
195
+ suppress_health_check=[HealthCheck(name) for name in data.get("suppress-health-check", [])],
196
+ seed=data.get("seed"),
197
+ wait_for_schema=data.get("wait-for-schema"),
198
+ max_failures=data.get("max-failures"),
199
+ reports=ReportsConfig.from_dict(data.get("reports", {})),
200
+ output=OutputConfig.from_dict(data.get("output", {})),
201
+ projects=ProjectsConfig.from_dict(data),
202
+ )
@@ -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)