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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /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
|