schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, unique
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ import hypothesis
8
+
9
+
10
+ @unique
11
+ class HealthCheck(str, Enum):
12
+ data_too_large = "data_too_large"
13
+ filter_too_much = "filter_too_much"
14
+ too_slow = "too_slow"
15
+ large_base_example = "large_base_example"
16
+ all = "all"
17
+
18
+ def as_hypothesis(self) -> list[hypothesis.HealthCheck]:
19
+ from hypothesis import HealthCheck
20
+
21
+ if self.name == "all":
22
+ return list(HealthCheck)
23
+
24
+ return [HealthCheck[self.name]]
@@ -0,0 +1,327 @@
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 SchemathesisWarning, resolve_warnings
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
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
+ return OperationConfig.from_hierarchy(configs)
72
+
73
+ def create_filter_set(
74
+ self,
75
+ *,
76
+ include_path: tuple[str, ...],
77
+ include_method: tuple[str, ...],
78
+ include_name: tuple[str, ...],
79
+ include_tag: tuple[str, ...],
80
+ include_operation_id: tuple[str, ...],
81
+ include_path_regex: str | None,
82
+ include_method_regex: str | None,
83
+ include_name_regex: str | None,
84
+ include_tag_regex: str | None,
85
+ include_operation_id_regex: str | None,
86
+ exclude_path: tuple[str, ...],
87
+ exclude_method: tuple[str, ...],
88
+ exclude_name: tuple[str, ...],
89
+ exclude_tag: tuple[str, ...],
90
+ exclude_operation_id: tuple[str, ...],
91
+ exclude_path_regex: str | None,
92
+ exclude_method_regex: str | None,
93
+ exclude_name_regex: str | None,
94
+ exclude_tag_regex: str | None,
95
+ exclude_operation_id_regex: str | None,
96
+ include_by: Callable | None,
97
+ exclude_by: Callable | None,
98
+ exclude_deprecated: bool,
99
+ ) -> FilterSet:
100
+ # Build explicit include filters
101
+ include_set = FilterSet()
102
+ if include_by:
103
+ include_set.include(include_by)
104
+ for name_ in include_name:
105
+ include_set.include(name=name_)
106
+ for method in include_method:
107
+ include_set.include(method=method)
108
+ for path in include_path:
109
+ include_set.include(path=path)
110
+ for tag in include_tag:
111
+ include_set.include(tag=tag)
112
+ for operation_id in include_operation_id:
113
+ include_set.include(operation_id=operation_id)
114
+ if (
115
+ include_name_regex
116
+ or include_method_regex
117
+ or include_path_regex
118
+ or include_tag_regex
119
+ or include_operation_id_regex
120
+ ):
121
+ include_set.include(
122
+ name_regex=include_name_regex,
123
+ method_regex=include_method_regex,
124
+ path_regex=include_path_regex,
125
+ tag_regex=include_tag_regex,
126
+ operation_id_regex=include_operation_id_regex,
127
+ )
128
+
129
+ # Build explicit exclude filters
130
+ exclude_set = FilterSet()
131
+ if exclude_by:
132
+ exclude_set.include(exclude_by)
133
+ for name_ in exclude_name:
134
+ exclude_set.include(name=name_)
135
+ for method in exclude_method:
136
+ exclude_set.include(method=method)
137
+ for path in exclude_path:
138
+ exclude_set.include(path=path)
139
+ for tag in exclude_tag:
140
+ exclude_set.include(tag=tag)
141
+ for operation_id in exclude_operation_id:
142
+ exclude_set.include(operation_id=operation_id)
143
+ if (
144
+ exclude_name_regex
145
+ or exclude_method_regex
146
+ or exclude_path_regex
147
+ or exclude_tag_regex
148
+ or exclude_operation_id_regex
149
+ ):
150
+ exclude_set.include(
151
+ name_regex=exclude_name_regex,
152
+ method_regex=exclude_method_regex,
153
+ path_regex=exclude_path_regex,
154
+ tag_regex=exclude_tag_regex,
155
+ operation_id_regex=exclude_operation_id_regex,
156
+ )
157
+
158
+ # Add deprecated operations to exclude filters if requested
159
+ if exclude_deprecated:
160
+ exclude_set.include(is_deprecated)
161
+
162
+ # Also update operations list for consistency with config structure
163
+ if not include_set.is_empty():
164
+ self.operations.insert(0, OperationConfig(filter_set=include_set, enabled=True))
165
+ if not exclude_set.is_empty():
166
+ self.operations.insert(0, OperationConfig(filter_set=exclude_set, enabled=False))
167
+
168
+ final = FilterSet()
169
+
170
+ # Get a stable reference to operations
171
+ operations = list(self.operations)
172
+
173
+ # Define a closure that implements our priority logic
174
+ def priority_filter(ctx: HasAPIOperation) -> bool:
175
+ """Filter operations according to CLI and config priority."""
176
+ # 1. CLI includes override everything if present
177
+ if not include_set.is_empty():
178
+ return include_set.match(ctx)
179
+
180
+ # 2. CLI excludes take precedence over config
181
+ if not exclude_set.is_empty() and exclude_set.match(ctx):
182
+ return False
183
+
184
+ # 3. Check config operations in priority order (first match wins)
185
+ for op_config in operations:
186
+ if op_config._filter_set.match(ctx):
187
+ return op_config.enabled
188
+
189
+ # 4. Default to include if no rule matches
190
+ return True
191
+
192
+ # Add our priority function as the filter
193
+ final.include(priority_filter)
194
+
195
+ return final
196
+
197
+
198
+ @dataclass
199
+ class OperationConfig(DiffBase):
200
+ _filter_set: FilterSet
201
+ enabled: bool
202
+ headers: dict | None
203
+ proxy: str | None
204
+ continue_on_failure: bool | None
205
+ tls_verify: bool | str | None
206
+ rate_limit: Limiter | None
207
+ request_timeout: float | int | None
208
+ request_cert: str | None
209
+ request_cert_key: str | None
210
+ parameters: dict[str, Any]
211
+ warnings: list[SchemathesisWarning] | None
212
+ auth: AuthConfig
213
+ checks: ChecksConfig
214
+ phases: PhasesConfig
215
+ generation: GenerationConfig
216
+
217
+ __slots__ = (
218
+ "_filter_set",
219
+ "enabled",
220
+ "headers",
221
+ "proxy",
222
+ "continue_on_failure",
223
+ "tls_verify",
224
+ "rate_limit",
225
+ "_rate_limit",
226
+ "request_timeout",
227
+ "request_cert",
228
+ "request_cert_key",
229
+ "parameters",
230
+ "warnings",
231
+ "auth",
232
+ "checks",
233
+ "phases",
234
+ "generation",
235
+ )
236
+
237
+ def __init__(
238
+ self,
239
+ *,
240
+ filter_set: FilterSet | None = None,
241
+ enabled: bool = True,
242
+ headers: dict | None = None,
243
+ proxy: str | None = None,
244
+ continue_on_failure: bool | None = None,
245
+ tls_verify: bool | str | None = None,
246
+ rate_limit: str | 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: bool | list[SchemathesisWarning] | 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.request_timeout = request_timeout
269
+ self.request_cert = request_cert
270
+ self.request_cert_key = request_cert_key
271
+ self.parameters = parameters or {}
272
+ self._set_warnings(warnings)
273
+ self.auth = auth or AuthConfig()
274
+ self.checks = checks or ChecksConfig()
275
+ self.phases = phases or PhasesConfig()
276
+ self.generation = generation or GenerationConfig()
277
+
278
+ @classmethod
279
+ def from_dict(cls, data: dict[str, Any]) -> OperationConfig:
280
+ filter_set = FilterSet()
281
+ for key_suffix, arg_suffix in (("", ""), ("-regex", "_regex")):
282
+ for attr, arg_name in FILTER_ATTRIBUTES:
283
+ key = f"include-{attr}{key_suffix}"
284
+ if key in data:
285
+ with reraise_filter_error(attr):
286
+ filter_set.include(**{f"{arg_name}{arg_suffix}": data[key]})
287
+ key = f"exclude-{attr}{key_suffix}"
288
+ if key in data:
289
+ with reraise_filter_error(attr):
290
+ filter_set.exclude(**{f"{arg_name}{arg_suffix}": data[key]})
291
+ for key, method in (("include-by", filter_set.include), ("exclude-by", filter_set.exclude)):
292
+ if key in data:
293
+ expression = data[key]
294
+ try:
295
+ func = expression_to_filter_function(expression)
296
+ method(func)
297
+ except ValueError:
298
+ raise ConfigError(f"Invalid filter expression: '{expression}'") from None
299
+
300
+ return cls(
301
+ filter_set=filter_set,
302
+ enabled=data.get("enabled", True),
303
+ headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
304
+ if "headers" in data
305
+ else None,
306
+ proxy=resolve(data.get("proxy")),
307
+ continue_on_failure=data.get("continue-on-failure", None),
308
+ tls_verify=resolve(data.get("tls-verify")),
309
+ rate_limit=resolve(data.get("rate-limit")),
310
+ request_timeout=data.get("request-timeout"),
311
+ request_cert=resolve(data.get("request-cert")),
312
+ request_cert_key=resolve(data.get("request-cert-key")),
313
+ parameters=load_parameters(data),
314
+ warnings=resolve_warnings(data.get("warnings")),
315
+ auth=AuthConfig.from_dict(data.get("auth", {})),
316
+ checks=ChecksConfig.from_dict(data.get("checks", {})),
317
+ phases=PhasesConfig.from_dict(data.get("phases", {})),
318
+ generation=GenerationConfig.from_dict(data.get("generation", {})),
319
+ )
320
+
321
+ def _set_warnings(self, warnings: bool | list[SchemathesisWarning] | None) -> None:
322
+ if warnings is False:
323
+ self.warnings = []
324
+ elif warnings is True:
325
+ self.warnings = list(SchemathesisWarning)
326
+ else:
327
+ self.warnings = warnings
@@ -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