schemathesis 3.15.4__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 (251) 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 -1219
  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 +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  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 +748 -82
  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 +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  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.15.4.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.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Callable, Generator
7
+
8
+ from schemathesis.config._auth import AuthConfig
9
+ from schemathesis.config._checks import ChecksConfig
10
+ from schemathesis.config._diff_base import DiffBase
11
+ from schemathesis.config._env import resolve
12
+ from schemathesis.config._error import ConfigError
13
+ from schemathesis.config._generation import GenerationConfig
14
+ from schemathesis.config._parameters import load_parameters
15
+ from schemathesis.config._phases import PhasesConfig
16
+ from schemathesis.config._rate_limit import build_limiter
17
+ from schemathesis.config._warnings import WarningsConfig
18
+ from schemathesis.core.errors import IncorrectUsage
19
+ from schemathesis.filters import FilterSet, HasAPIOperation, expression_to_filter_function, is_deprecated
20
+
21
+ if TYPE_CHECKING:
22
+ from pyrate_limiter import Limiter
23
+
24
+ from schemathesis.schemas import APIOperation
25
+
26
+ FILTER_ATTRIBUTES = [
27
+ ("name", "name"),
28
+ ("method", "method"),
29
+ ("path", "path"),
30
+ ("tag", "tag"),
31
+ ("operation-id", "operation_id"),
32
+ ]
33
+
34
+
35
+ @contextmanager
36
+ def reraise_filter_error(attr: str) -> Generator:
37
+ try:
38
+ yield
39
+ except IncorrectUsage as exc:
40
+ if str(exc) == "Filter already exists":
41
+ raise ConfigError(
42
+ f"Filter for '{attr}' already exists. You can't simultaneously include and exclude the same thing."
43
+ ) from None
44
+ raise
45
+ except re.error as exc:
46
+ raise ConfigError(
47
+ f"Filter for '{attr}' contains an invalid regular expression: {exc.pattern!r}\n\n {exc}"
48
+ ) from None
49
+
50
+
51
+ @dataclass
52
+ class OperationsConfig(DiffBase):
53
+ operations: list[OperationConfig]
54
+
55
+ __slots__ = ("operations",)
56
+
57
+ def __init__(self, *, operations: list[OperationConfig] | None = None):
58
+ self.operations = operations or []
59
+
60
+ def __repr__(self) -> str:
61
+ if self.operations:
62
+ return f"[{', '.join(DiffBase.__repr__(cfg) for cfg in self.operations)}]"
63
+ return "[]"
64
+
65
+ @classmethod
66
+ def from_hierarchy(cls, configs: list[OperationsConfig]) -> OperationsConfig: # type: ignore[override]
67
+ return cls(operations=sum([config.operations for config in reversed(configs)], []))
68
+
69
+ def get_for_operation(self, operation: APIOperation) -> OperationConfig:
70
+ configs = [config for config in self.operations if config._filter_set.applies_to(operation)]
71
+ if not configs:
72
+ return OperationConfig()
73
+ return OperationConfig.from_hierarchy(configs)
74
+
75
+ def create_filter_set(
76
+ self,
77
+ *,
78
+ include_path: tuple[str, ...],
79
+ include_method: tuple[str, ...],
80
+ include_name: tuple[str, ...],
81
+ include_tag: tuple[str, ...],
82
+ include_operation_id: tuple[str, ...],
83
+ include_path_regex: str | None,
84
+ include_method_regex: str | None,
85
+ include_name_regex: str | None,
86
+ include_tag_regex: str | None,
87
+ include_operation_id_regex: str | None,
88
+ exclude_path: tuple[str, ...],
89
+ exclude_method: tuple[str, ...],
90
+ exclude_name: tuple[str, ...],
91
+ exclude_tag: tuple[str, ...],
92
+ exclude_operation_id: tuple[str, ...],
93
+ exclude_path_regex: str | None,
94
+ exclude_method_regex: str | None,
95
+ exclude_name_regex: str | None,
96
+ exclude_tag_regex: str | None,
97
+ exclude_operation_id_regex: str | None,
98
+ include_by: Callable | None,
99
+ exclude_by: Callable | None,
100
+ exclude_deprecated: bool,
101
+ ) -> FilterSet:
102
+ # Build explicit include filters
103
+ include_set = FilterSet()
104
+ if include_by:
105
+ include_set.include(include_by)
106
+ for name_ in include_name:
107
+ include_set.include(name=name_)
108
+ for method in include_method:
109
+ include_set.include(method=method)
110
+ for path in include_path:
111
+ include_set.include(path=path)
112
+ for tag in include_tag:
113
+ include_set.include(tag=tag)
114
+ for operation_id in include_operation_id:
115
+ include_set.include(operation_id=operation_id)
116
+ if (
117
+ include_name_regex
118
+ or include_method_regex
119
+ or include_path_regex
120
+ or include_tag_regex
121
+ or include_operation_id_regex
122
+ ):
123
+ include_set.include(
124
+ name_regex=include_name_regex,
125
+ method_regex=include_method_regex,
126
+ path_regex=include_path_regex,
127
+ tag_regex=include_tag_regex,
128
+ operation_id_regex=include_operation_id_regex,
129
+ )
130
+
131
+ # Build explicit exclude filters
132
+ exclude_set = FilterSet()
133
+ if exclude_by:
134
+ exclude_set.include(exclude_by)
135
+ for name_ in exclude_name:
136
+ exclude_set.include(name=name_)
137
+ for method in exclude_method:
138
+ exclude_set.include(method=method)
139
+ for path in exclude_path:
140
+ exclude_set.include(path=path)
141
+ for tag in exclude_tag:
142
+ exclude_set.include(tag=tag)
143
+ for operation_id in exclude_operation_id:
144
+ exclude_set.include(operation_id=operation_id)
145
+ if (
146
+ exclude_name_regex
147
+ or exclude_method_regex
148
+ or exclude_path_regex
149
+ or exclude_tag_regex
150
+ or exclude_operation_id_regex
151
+ ):
152
+ exclude_set.include(
153
+ name_regex=exclude_name_regex,
154
+ method_regex=exclude_method_regex,
155
+ path_regex=exclude_path_regex,
156
+ tag_regex=exclude_tag_regex,
157
+ operation_id_regex=exclude_operation_id_regex,
158
+ )
159
+
160
+ # Add deprecated operations to exclude filters if requested
161
+ if exclude_deprecated:
162
+ exclude_set.include(is_deprecated)
163
+
164
+ return self.filter_set_with(include=include_set, exclude=exclude_set)
165
+
166
+ def filter_set_with(self, include: FilterSet, exclude: FilterSet | None = None) -> FilterSet:
167
+ if not self.operations and exclude is None:
168
+ return include
169
+ operations = list(self.operations)
170
+
171
+ final = FilterSet()
172
+ exclude = exclude or FilterSet()
173
+
174
+ def priority_filter(ctx: HasAPIOperation) -> bool:
175
+ """Filter operations according to CLI and config priority."""
176
+ for op_config in operations:
177
+ if op_config._filter_set.match(ctx) and not op_config.enabled:
178
+ return False
179
+
180
+ if not include.is_empty():
181
+ if exclude.is_empty():
182
+ return include.match(ctx)
183
+ return include.match(ctx) and not exclude.match(ctx)
184
+ elif not exclude.is_empty():
185
+ return not exclude.match(ctx)
186
+
187
+ return True
188
+
189
+ # Add our priority function as the filter
190
+ final.include(priority_filter)
191
+
192
+ return final
193
+
194
+
195
+ @dataclass
196
+ class OperationConfig(DiffBase):
197
+ _filter_set: FilterSet
198
+ enabled: bool
199
+ headers: dict | None
200
+ proxy: str | None
201
+ continue_on_failure: bool | None
202
+ tls_verify: bool | str | None
203
+ rate_limit: Limiter | None
204
+ max_redirects: int | None
205
+ request_timeout: float | int | None
206
+ request_cert: str | None
207
+ request_cert_key: str | None
208
+ parameters: dict[str, Any]
209
+ warnings: WarningsConfig | None
210
+ auth: AuthConfig
211
+ checks: ChecksConfig
212
+ phases: PhasesConfig
213
+ generation: GenerationConfig
214
+
215
+ __slots__ = (
216
+ "_filter_set",
217
+ "enabled",
218
+ "headers",
219
+ "proxy",
220
+ "continue_on_failure",
221
+ "tls_verify",
222
+ "rate_limit",
223
+ "_rate_limit",
224
+ "max_redirects",
225
+ "request_timeout",
226
+ "request_cert",
227
+ "request_cert_key",
228
+ "parameters",
229
+ "warnings",
230
+ "auth",
231
+ "checks",
232
+ "phases",
233
+ "generation",
234
+ )
235
+
236
+ def __init__(
237
+ self,
238
+ *,
239
+ filter_set: FilterSet | None = None,
240
+ enabled: bool = True,
241
+ headers: dict | None = None,
242
+ proxy: str | None = None,
243
+ continue_on_failure: bool | None = None,
244
+ tls_verify: bool | str | None = None,
245
+ rate_limit: str | None = None,
246
+ max_redirects: int | None = None,
247
+ request_timeout: float | int | None = None,
248
+ request_cert: str | None = None,
249
+ request_cert_key: str | None = None,
250
+ parameters: dict[str, Any] | None = None,
251
+ warnings: WarningsConfig | None = None,
252
+ auth: AuthConfig | None = None,
253
+ checks: ChecksConfig | None = None,
254
+ phases: PhasesConfig | None = None,
255
+ generation: GenerationConfig | None = None,
256
+ ) -> None:
257
+ self._filter_set = filter_set or FilterSet()
258
+ self.enabled = enabled
259
+ self.headers = headers
260
+ self.proxy = proxy
261
+ self.continue_on_failure = continue_on_failure
262
+ self.tls_verify = tls_verify
263
+ if rate_limit is not None:
264
+ self.rate_limit = build_limiter(rate_limit)
265
+ else:
266
+ self.rate_limit = rate_limit
267
+ self._rate_limit = rate_limit
268
+ self.max_redirects = max_redirects
269
+ self.request_timeout = request_timeout
270
+ self.request_cert = request_cert
271
+ self.request_cert_key = request_cert_key
272
+ self.parameters = parameters or {}
273
+ self.warnings = warnings
274
+ self.auth = auth or AuthConfig()
275
+ self.checks = checks or ChecksConfig()
276
+ self.phases = phases or PhasesConfig()
277
+ self.generation = generation or GenerationConfig()
278
+
279
+ @classmethod
280
+ def from_dict(cls, data: dict[str, Any]) -> OperationConfig:
281
+ filter_set = FilterSet()
282
+ seen = set()
283
+ for key_suffix, arg_suffix in (("", ""), ("-regex", "_regex")):
284
+ for attr, arg_name in FILTER_ATTRIBUTES:
285
+ key = f"include-{attr}{key_suffix}"
286
+ if key in data:
287
+ seen.add(key)
288
+ with reraise_filter_error(attr):
289
+ filter_set.include(**{f"{arg_name}{arg_suffix}": data[key]})
290
+ key = f"exclude-{attr}{key_suffix}"
291
+ if key in data:
292
+ seen.add(key)
293
+ with reraise_filter_error(attr):
294
+ filter_set.exclude(**{f"{arg_name}{arg_suffix}": data[key]})
295
+ for key, method in (("include-by", filter_set.include), ("exclude-by", filter_set.exclude)):
296
+ if key in data:
297
+ seen.add(key)
298
+ expression = data[key]
299
+ try:
300
+ func = expression_to_filter_function(expression)
301
+ method(func)
302
+ except ValueError:
303
+ raise ConfigError(f"Invalid filter expression: '{expression}'") from None
304
+ if not set(data) - seen:
305
+ raise ConfigError("Operation filters defined, but no settings are being overridden")
306
+
307
+ warnings_value = data.get("warnings")
308
+ # If warnings is True or None, keep it as None to inherit from project level
309
+ # Only create a WarningsConfig if it's False or a specific list/dict
310
+ if warnings_value is True or warnings_value is None:
311
+ warnings = None
312
+ else:
313
+ warnings = WarningsConfig.from_value(warnings_value)
314
+
315
+ return cls(
316
+ filter_set=filter_set,
317
+ enabled=data.get("enabled", True),
318
+ headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
319
+ if "headers" in data
320
+ else None,
321
+ proxy=resolve(data.get("proxy")),
322
+ continue_on_failure=data.get("continue-on-failure", None),
323
+ tls_verify=resolve(data.get("tls-verify")),
324
+ rate_limit=resolve(data.get("rate-limit")),
325
+ max_redirects=data.get("max-redirects"),
326
+ request_timeout=data.get("request-timeout"),
327
+ request_cert=resolve(data.get("request-cert")),
328
+ request_cert_key=resolve(data.get("request-cert-key")),
329
+ parameters=load_parameters(data),
330
+ warnings=warnings,
331
+ auth=AuthConfig.from_dict(data.get("auth", {})),
332
+ checks=ChecksConfig.from_dict(data.get("checks", {})),
333
+ phases=PhasesConfig.from_dict(data.get("phases", {})),
334
+ generation=GenerationConfig.from_dict(data.get("generation", {})),
335
+ )
@@ -0,0 +1,171 @@
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
+
8
+ # Exact keys to sanitize
9
+ DEFAULT_KEYS_TO_SANITIZE = [
10
+ "phpsessid",
11
+ "xsrf-token",
12
+ "_csrf",
13
+ "_csrf_token",
14
+ "_session",
15
+ "_xsrf",
16
+ "aiohttp_session",
17
+ "api_key",
18
+ "api-key",
19
+ "apikey",
20
+ "auth",
21
+ "authorization",
22
+ "connect.sid",
23
+ "cookie",
24
+ "credentials",
25
+ "csrf",
26
+ "csrf_token",
27
+ "csrf-token",
28
+ "csrftoken",
29
+ "ip_address",
30
+ "mysql_pwd",
31
+ "passwd",
32
+ "password",
33
+ "private_key",
34
+ "private-key",
35
+ "privatekey",
36
+ "remote_addr",
37
+ "remote-addr",
38
+ "secret",
39
+ "session",
40
+ "sessionid",
41
+ "set_cookie",
42
+ "set-cookie",
43
+ "token",
44
+ "x_api_key",
45
+ "x-api-key",
46
+ "x_csrftoken",
47
+ "x-csrftoken",
48
+ "x_forwarded_for",
49
+ "x-forwarded-for",
50
+ "x_real_ip",
51
+ "x-real-ip",
52
+ ]
53
+
54
+ # Markers indicating potentially sensitive keys
55
+ DEFAULT_SENSITIVE_MARKERS = [
56
+ "token",
57
+ "key",
58
+ "secret",
59
+ "password",
60
+ "auth",
61
+ "session",
62
+ "passwd",
63
+ "credential",
64
+ ]
65
+
66
+ DEFAULT_REPLACEMENT = "[Filtered]"
67
+
68
+
69
+ @dataclass(repr=False)
70
+ class SanitizationConfig(DiffBase):
71
+ """Configuration for sanitizing sensitive data."""
72
+
73
+ enabled: bool
74
+ keys_to_sanitize: list[str]
75
+ sensitive_markers: list[str]
76
+ replacement: str
77
+
78
+ __slots__ = ("enabled", "keys_to_sanitize", "sensitive_markers", "replacement")
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ enabled: bool = True,
84
+ keys_to_sanitize: list[str] | None = None,
85
+ sensitive_markers: list[str] | None = None,
86
+ replacement: str | None = None,
87
+ ) -> None:
88
+ self.enabled = enabled
89
+ self.keys_to_sanitize = keys_to_sanitize or DEFAULT_KEYS_TO_SANITIZE
90
+ self.sensitive_markers = sensitive_markers or DEFAULT_SENSITIVE_MARKERS
91
+ self.replacement = replacement or DEFAULT_REPLACEMENT
92
+
93
+ @classmethod
94
+ def from_dict(cls, data: dict[str, Any]) -> SanitizationConfig:
95
+ return cls(
96
+ enabled=data.get("enabled", True),
97
+ keys_to_sanitize=[k.lower() for k in data.get("keys-to-sanitize", [])] or DEFAULT_KEYS_TO_SANITIZE,
98
+ sensitive_markers=[m.lower() for m in data.get("sensitive-markers", [])] or DEFAULT_SENSITIVE_MARKERS,
99
+ replacement=data.get("replacement", DEFAULT_REPLACEMENT),
100
+ )
101
+
102
+ def update(self, *, enabled: bool | None = None) -> None:
103
+ if enabled is not None:
104
+ self.enabled = enabled
105
+
106
+
107
+ MAX_PAYLOAD_SIZE = 512
108
+ MAX_LINES = 10
109
+ MAX_WIDTH = 80
110
+
111
+
112
+ @dataclass(repr=False)
113
+ class TruncationConfig(DiffBase):
114
+ """Configuration for truncating large output."""
115
+
116
+ enabled: bool
117
+ max_payload_size: int
118
+ max_lines: int
119
+ max_width: int
120
+
121
+ __slots__ = ("enabled", "max_payload_size", "max_lines", "max_width")
122
+
123
+ def __init__(
124
+ self,
125
+ *,
126
+ enabled: bool = True,
127
+ max_payload_size: int = MAX_PAYLOAD_SIZE,
128
+ max_lines: int = MAX_LINES,
129
+ max_width: int = MAX_WIDTH,
130
+ ) -> None:
131
+ self.enabled = enabled
132
+ self.max_payload_size = max_payload_size
133
+ self.max_lines = max_lines
134
+ self.max_width = max_width
135
+
136
+ @classmethod
137
+ def from_dict(cls, data: dict[str, Any]) -> TruncationConfig:
138
+ return cls(
139
+ enabled=data.get("enabled", True),
140
+ max_payload_size=data.get("max-payload-size", MAX_PAYLOAD_SIZE),
141
+ max_lines=data.get("max-lines", MAX_LINES),
142
+ max_width=data.get("max-width", MAX_WIDTH),
143
+ )
144
+
145
+ def update(self, *, enabled: bool | None = None) -> None:
146
+ if enabled is not None:
147
+ self.enabled = enabled
148
+
149
+
150
+ @dataclass(repr=False)
151
+ class OutputConfig(DiffBase):
152
+ sanitization: SanitizationConfig
153
+ truncation: TruncationConfig
154
+
155
+ __slots__ = ("sanitization", "truncation")
156
+
157
+ def __init__(
158
+ self,
159
+ *,
160
+ sanitization: SanitizationConfig | None = None,
161
+ truncation: TruncationConfig | None = None,
162
+ ) -> None:
163
+ self.sanitization = sanitization or SanitizationConfig()
164
+ self.truncation = truncation or TruncationConfig()
165
+
166
+ @classmethod
167
+ def from_dict(cls, data: dict[str, Any]) -> OutputConfig:
168
+ return cls(
169
+ sanitization=SanitizationConfig.from_dict(data.get("sanitization", {})),
170
+ truncation=TruncationConfig.from_dict(data.get("truncation", {})),
171
+ )
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from schemathesis.config._env import resolve
6
+
7
+ # Define valid parameter locations from OpenAPI
8
+ ParameterLocation = Literal["path", "query", "header", "cookie", "body"]
9
+ VALID_LOCATIONS: list[ParameterLocation] = ["path", "query", "header", "cookie", "body"]
10
+
11
+
12
+ def load_parameters(data: dict[str, Any]) -> dict[str, Any]:
13
+ parameters = {}
14
+ for key, value in data.get("parameters", {}).items():
15
+ if isinstance(value, str):
16
+ parameters[key] = resolve(value)
17
+ else:
18
+ parameters[key] = value
19
+ return parameters