schemathesis 4.0.0a9__py3-none-any.whl → 4.0.0a11__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 +3 -7
- schemathesis/checks.py +17 -7
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +147 -260
- schemathesis/cli/commands/run/context.py +2 -3
- schemathesis/cli/commands/run/events.py +4 -0
- schemathesis/cli/commands/run/executor.py +60 -73
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +6 -5
- schemathesis/cli/commands/run/handlers/output.py +26 -47
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +36 -161
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +188 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +150 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +313 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +151 -0
- schemathesis/config/_projects.py +495 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +116 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/schema.json +837 -0
- schemathesis/core/__init__.py +3 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +19 -2
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +41 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +8 -8
- schemathesis/engine/phases/stateful/_executor.py +68 -43
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +77 -17
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +6 -31
- schemathesis/generation/case.py +5 -3
- schemathesis/generation/coverage.py +174 -134
- schemathesis/generation/hypothesis/__init__.py +7 -1
- schemathesis/generation/hypothesis/builder.py +40 -14
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/state_machine.py +8 -1
- schemathesis/graphql/loaders.py +21 -12
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +22 -13
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/plugin.py +11 -2
- schemathesis/schemas.py +13 -61
- schemathesis/specs/graphql/schemas.py +11 -15
- schemathesis/specs/openapi/_hypothesis.py +12 -8
- schemathesis/specs/openapi/checks.py +16 -18
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +11 -20
- schemathesis/specs/openapi/stateful/__init__.py +10 -5
- schemathesis/transport/prepare.py +7 -6
- schemathesis/transport/requests.py +3 -1
- schemathesis/transport/wsgi.py +3 -4
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
- schemathesis-4.0.0a11.dist-info/RECORD +166 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis-4.0.0a9.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import difflib
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
from schemathesis.core.errors import SchemathesisError
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from jsonschema import ValidationError
|
10
|
+
|
11
|
+
|
12
|
+
class ConfigError(SchemathesisError):
|
13
|
+
"""Invalid configuration."""
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def from_validation_error(cls, error: ValidationError) -> ConfigError:
|
17
|
+
message = error.message
|
18
|
+
if error.validator == "enum":
|
19
|
+
message = _format_enum_error(error)
|
20
|
+
elif error.validator == "minimum":
|
21
|
+
message = _format_minimum_error(error)
|
22
|
+
elif error.validator == "required":
|
23
|
+
message = _format_required_error(error)
|
24
|
+
elif error.validator == "type":
|
25
|
+
message = _format_type_error(error)
|
26
|
+
elif error.validator == "additionalProperties":
|
27
|
+
message = _format_additional_properties_error(error)
|
28
|
+
elif error.validator == "anyOf":
|
29
|
+
message = _format_anyof_error(error)
|
30
|
+
return cls(message)
|
31
|
+
|
32
|
+
|
33
|
+
def _format_minimum_error(error: ValidationError) -> str:
|
34
|
+
assert isinstance(error.validator_value, (int, float))
|
35
|
+
section = path_to_section_name(list(error.path)[:-1] if error.path else [])
|
36
|
+
assert error.path
|
37
|
+
|
38
|
+
prop_name = error.path[-1]
|
39
|
+
min_value = error.validator_value
|
40
|
+
actual_value = error.instance
|
41
|
+
|
42
|
+
return (
|
43
|
+
f"Error in {section} section:\n Value too low:\n\n"
|
44
|
+
f" - '{prop_name}' → Must be at least {min_value}, but got {actual_value}."
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def _format_required_error(error: ValidationError) -> str:
|
49
|
+
assert isinstance(error.validator_value, list)
|
50
|
+
missing_keys = sorted(set(error.validator_value) - set(error.instance))
|
51
|
+
|
52
|
+
section = path_to_section_name(list(error.path))
|
53
|
+
|
54
|
+
details = "\n".join(f" - '{key}'" for key in missing_keys)
|
55
|
+
return f"Error in {section} section:\n Missing required properties:\n\n{details}\n\n"
|
56
|
+
|
57
|
+
|
58
|
+
def _format_enum_error(error: ValidationError) -> str:
|
59
|
+
assert isinstance(error.validator_value, list)
|
60
|
+
valid_values = sorted(error.validator_value)
|
61
|
+
|
62
|
+
path = list(error.path)
|
63
|
+
|
64
|
+
if path and isinstance(path[-1], int):
|
65
|
+
idx = path[-1]
|
66
|
+
prop_name = path[-2]
|
67
|
+
section_path = path[:-2]
|
68
|
+
description = f"Item #{idx} in the '{prop_name}' array"
|
69
|
+
else:
|
70
|
+
prop_name = path[-1] if path else "value"
|
71
|
+
section_path = path[:-1]
|
72
|
+
description = f"'{prop_name}'"
|
73
|
+
|
74
|
+
suggestion = ""
|
75
|
+
if isinstance(error.instance, str) and all(isinstance(v, str) for v in valid_values):
|
76
|
+
match = _find_closest_match(error.instance, valid_values)
|
77
|
+
if match:
|
78
|
+
suggestion = f" Did you mean '{match}'?"
|
79
|
+
|
80
|
+
section = path_to_section_name(section_path)
|
81
|
+
valid_values_str = ", ".join(repr(v) for v in valid_values)
|
82
|
+
return (
|
83
|
+
f"Error in {section} section:\n Invalid value:\n\n"
|
84
|
+
f" - {description} → '{error.instance}' is not a valid value.{suggestion}\n\n"
|
85
|
+
f"Valid values are: {valid_values_str}."
|
86
|
+
)
|
87
|
+
|
88
|
+
|
89
|
+
def _format_type_error(error: ValidationError) -> str:
|
90
|
+
expected = error.validator_value
|
91
|
+
assert isinstance(expected, (str, list))
|
92
|
+
section = path_to_section_name(list(error.path)[:-1] if error.path else [])
|
93
|
+
assert error.path
|
94
|
+
|
95
|
+
type_phrases = {
|
96
|
+
"object": "an object",
|
97
|
+
"array": "an array",
|
98
|
+
"number": "a number",
|
99
|
+
"boolean": "a boolean",
|
100
|
+
"string": "a string",
|
101
|
+
"integer": "an integer",
|
102
|
+
"null": "null",
|
103
|
+
}
|
104
|
+
message = f"Error in {section} section:\n Type error:\n\n - '{error.path[-1]}' → Must be "
|
105
|
+
|
106
|
+
if isinstance(expected, list):
|
107
|
+
message += f"one of: {' or '.join(expected)}"
|
108
|
+
else:
|
109
|
+
message += type_phrases[expected]
|
110
|
+
actual = type(error.instance).__name__
|
111
|
+
message += f", but got {actual}: {error.instance}"
|
112
|
+
return message
|
113
|
+
|
114
|
+
|
115
|
+
def _format_additional_properties_error(error: ValidationError) -> str:
|
116
|
+
valid = list(error.schema.get("properties", {}))
|
117
|
+
unknown = sorted(set(error.instance) - set(valid))
|
118
|
+
valid_list = ", ".join(f"'{prop}'" for prop in valid)
|
119
|
+
section = path_to_section_name(list(error.path))
|
120
|
+
|
121
|
+
details = []
|
122
|
+
for prop in unknown:
|
123
|
+
match = _find_closest_match(prop, valid)
|
124
|
+
if match:
|
125
|
+
details.append(f"- '{prop}' → Did you mean '{match}'?")
|
126
|
+
else:
|
127
|
+
details.append(f"- '{prop}'")
|
128
|
+
|
129
|
+
return (
|
130
|
+
f"Error in {section} section:\n Unknown properties:\n\n"
|
131
|
+
+ "\n".join(f" {detail}" for detail in details)
|
132
|
+
+ f"\n\nValid properties for {section} are: {valid_list}."
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
def _format_anyof_error(error: ValidationError) -> str:
|
137
|
+
if list(error.schema_path) == ["properties", "operations", "items", "anyOf"]:
|
138
|
+
section = path_to_section_name(list(error.path))
|
139
|
+
return (
|
140
|
+
f"Error in {section} section:\n At least one filter is required when defining [[operations]].\n\n"
|
141
|
+
"Please specify at least one include or exclude filter property (e.g., include-path, exclude-tag, etc.)."
|
142
|
+
)
|
143
|
+
return error.message
|
144
|
+
|
145
|
+
|
146
|
+
def path_to_section_name(path: list[int | str]) -> str:
|
147
|
+
"""Convert a JSON path to a TOML-like section name."""
|
148
|
+
if not path:
|
149
|
+
return "root"
|
150
|
+
|
151
|
+
return f"[{'.'.join(str(p) for p in path)}]"
|
152
|
+
|
153
|
+
|
154
|
+
def _find_closest_match(value: str, variants: list[str]) -> str | None:
|
155
|
+
matches = difflib.get_close_matches(value, variants, n=1, cutoff=0.6)
|
156
|
+
return matches[0] if matches else None
|
@@ -0,0 +1,150 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import TYPE_CHECKING, Any
|
5
|
+
|
6
|
+
from schemathesis.config._diff_base import DiffBase
|
7
|
+
from schemathesis.generation.modes import GenerationMode
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from schemathesis.generation.targets import TargetFunction
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass(repr=False)
|
14
|
+
class GenerationConfig(DiffBase):
|
15
|
+
modes: list[GenerationMode]
|
16
|
+
max_examples: int | None
|
17
|
+
no_shrink: bool
|
18
|
+
deterministic: bool
|
19
|
+
# Allow generating `\x00` bytes in strings
|
20
|
+
allow_x00: bool
|
21
|
+
# Generate strings using the given codec
|
22
|
+
codec: str | None
|
23
|
+
maximize: list[TargetFunction]
|
24
|
+
# Whether to generate security parameters
|
25
|
+
with_security_parameters: bool
|
26
|
+
# Allowing using `null` for optional arguments in GraphQL queries
|
27
|
+
graphql_allow_null: bool
|
28
|
+
database: str | None
|
29
|
+
unique_inputs: bool
|
30
|
+
exclude_header_characters: str | None
|
31
|
+
|
32
|
+
__slots__ = (
|
33
|
+
"modes",
|
34
|
+
"max_examples",
|
35
|
+
"no_shrink",
|
36
|
+
"deterministic",
|
37
|
+
"allow_x00",
|
38
|
+
"codec",
|
39
|
+
"maximize",
|
40
|
+
"with_security_parameters",
|
41
|
+
"graphql_allow_null",
|
42
|
+
"database",
|
43
|
+
"unique_inputs",
|
44
|
+
"exclude_header_characters",
|
45
|
+
)
|
46
|
+
|
47
|
+
def __init__(
|
48
|
+
self,
|
49
|
+
*,
|
50
|
+
modes: list[GenerationMode] | None = None,
|
51
|
+
max_examples: int | None = None,
|
52
|
+
no_shrink: bool = False,
|
53
|
+
deterministic: bool = False,
|
54
|
+
allow_x00: bool = True,
|
55
|
+
codec: str | None = "utf-8",
|
56
|
+
maximize: list[TargetFunction] | None = None,
|
57
|
+
with_security_parameters: bool = True,
|
58
|
+
graphql_allow_null: bool = True,
|
59
|
+
database: str | None = None,
|
60
|
+
unique_inputs: bool = False,
|
61
|
+
exclude_header_characters: str | None = None,
|
62
|
+
) -> None:
|
63
|
+
from schemathesis.generation import GenerationMode
|
64
|
+
|
65
|
+
# TODO: Switch to `all` by default.
|
66
|
+
self.modes = modes or [GenerationMode.POSITIVE]
|
67
|
+
self.max_examples = max_examples
|
68
|
+
self.no_shrink = no_shrink
|
69
|
+
self.deterministic = deterministic
|
70
|
+
self.allow_x00 = allow_x00
|
71
|
+
self.codec = codec
|
72
|
+
self.maximize = maximize or []
|
73
|
+
self.with_security_parameters = with_security_parameters
|
74
|
+
self.graphql_allow_null = graphql_allow_null
|
75
|
+
self.database = database
|
76
|
+
self.unique_inputs = unique_inputs
|
77
|
+
self.exclude_header_characters = exclude_header_characters
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def from_dict(cls, data: dict[str, Any]) -> GenerationConfig:
|
81
|
+
mode_raw = data.get("mode")
|
82
|
+
if mode_raw == "all":
|
83
|
+
modes = GenerationMode.all()
|
84
|
+
elif mode_raw is not None:
|
85
|
+
modes = [GenerationMode(mode_raw)]
|
86
|
+
else:
|
87
|
+
modes = None
|
88
|
+
maximize = _get_maximize(data.get("maximize"))
|
89
|
+
return cls(
|
90
|
+
modes=modes,
|
91
|
+
max_examples=data.get("max-examples"),
|
92
|
+
no_shrink=data.get("no-shrink", False),
|
93
|
+
deterministic=data.get("deterministic", False),
|
94
|
+
allow_x00=data.get("allow-x00", True),
|
95
|
+
codec=data.get("codec", "utf-8"),
|
96
|
+
maximize=maximize,
|
97
|
+
with_security_parameters=data.get("with-security-parameters", True),
|
98
|
+
graphql_allow_null=data.get("graphql-allow-null", True),
|
99
|
+
database=data.get("database"),
|
100
|
+
unique_inputs=data.get("unique-inputs", False),
|
101
|
+
exclude_header_characters=data.get("exclude-header-characters"),
|
102
|
+
)
|
103
|
+
|
104
|
+
def update(
|
105
|
+
self,
|
106
|
+
*,
|
107
|
+
modes: list[GenerationMode] | None = None,
|
108
|
+
max_examples: int | None = None,
|
109
|
+
no_shrink: bool = False,
|
110
|
+
deterministic: bool | None = None,
|
111
|
+
allow_x00: bool = True,
|
112
|
+
codec: str | None = None,
|
113
|
+
maximize: list[TargetFunction] | None = None,
|
114
|
+
with_security_parameters: bool | None = None,
|
115
|
+
graphql_allow_null: bool = True,
|
116
|
+
database: str | None = None,
|
117
|
+
unique_inputs: bool = False,
|
118
|
+
exclude_header_characters: str | None = None,
|
119
|
+
) -> None:
|
120
|
+
if modes is not None:
|
121
|
+
self.modes = modes
|
122
|
+
if max_examples is not None:
|
123
|
+
self.max_examples = max_examples
|
124
|
+
self.no_shrink = no_shrink
|
125
|
+
self.deterministic = deterministic or False
|
126
|
+
self.allow_x00 = allow_x00
|
127
|
+
if codec is not None:
|
128
|
+
self.codec = codec
|
129
|
+
if maximize is not None:
|
130
|
+
self.maximize = maximize
|
131
|
+
if with_security_parameters is not None:
|
132
|
+
self.with_security_parameters = with_security_parameters
|
133
|
+
self.graphql_allow_null = graphql_allow_null
|
134
|
+
if database is not None:
|
135
|
+
self.database = database
|
136
|
+
self.unique_inputs = unique_inputs
|
137
|
+
if exclude_header_characters is not None:
|
138
|
+
self.exclude_header_characters = exclude_header_characters
|
139
|
+
|
140
|
+
|
141
|
+
def _get_maximize(value: Any) -> list[TargetFunction]:
|
142
|
+
from schemathesis.generation.targets import TARGETS
|
143
|
+
|
144
|
+
if isinstance(value, list):
|
145
|
+
targets = value
|
146
|
+
elif isinstance(value, str):
|
147
|
+
targets = [value]
|
148
|
+
else:
|
149
|
+
targets = []
|
150
|
+
return TARGETS.get_by_names(targets)
|
@@ -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,313 @@
|
|
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.core.errors import IncorrectUsage
|
18
|
+
from schemathesis.filters import FilterSet, HasAPIOperation, expression_to_filter_function, is_deprecated
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from pyrate_limiter import Limiter
|
22
|
+
|
23
|
+
from schemathesis.schemas import APIOperation
|
24
|
+
|
25
|
+
FILTER_ATTRIBUTES = [
|
26
|
+
("name", "name"),
|
27
|
+
("method", "method"),
|
28
|
+
("path", "path"),
|
29
|
+
("tag", "tag"),
|
30
|
+
("operation-id", "operation_id"),
|
31
|
+
]
|
32
|
+
|
33
|
+
|
34
|
+
@contextmanager
|
35
|
+
def reraise_filter_error(attr: str) -> Generator:
|
36
|
+
try:
|
37
|
+
yield
|
38
|
+
except IncorrectUsage as exc:
|
39
|
+
if str(exc) == "Filter already exists":
|
40
|
+
raise ConfigError(
|
41
|
+
f"Filter for '{attr}' already exists. You can't simultaneously include and exclude the same thing."
|
42
|
+
) from None
|
43
|
+
raise
|
44
|
+
except re.error as exc:
|
45
|
+
raise ConfigError(
|
46
|
+
f"Filter for '{attr}' contains an invalid regular expression: {exc.pattern!r}\n\n {exc}"
|
47
|
+
) from None
|
48
|
+
|
49
|
+
|
50
|
+
@dataclass
|
51
|
+
class OperationsConfig(DiffBase):
|
52
|
+
operations: list[OperationConfig]
|
53
|
+
|
54
|
+
__slots__ = ("operations",)
|
55
|
+
|
56
|
+
def __init__(self, *, operations: list[OperationConfig] | None = None):
|
57
|
+
self.operations = operations or []
|
58
|
+
|
59
|
+
def __repr__(self) -> str:
|
60
|
+
if self.operations:
|
61
|
+
return f"[{', '.join(DiffBase.__repr__(cfg) for cfg in self.operations)}]"
|
62
|
+
return "[]"
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def from_hierarchy(cls, configs: list[OperationsConfig]) -> OperationsConfig: # type: ignore
|
66
|
+
return cls(operations=sum([config.operations for config in reversed(configs)], []))
|
67
|
+
|
68
|
+
def get_for_operation(self, operation: APIOperation) -> OperationConfig:
|
69
|
+
configs = [config for config in self.operations if config._filter_set.applies_to(operation)]
|
70
|
+
return OperationConfig.from_hierarchy(configs)
|
71
|
+
|
72
|
+
def create_filter_set(
|
73
|
+
self,
|
74
|
+
*,
|
75
|
+
include_path: tuple[str, ...],
|
76
|
+
include_method: tuple[str, ...],
|
77
|
+
include_name: tuple[str, ...],
|
78
|
+
include_tag: tuple[str, ...],
|
79
|
+
include_operation_id: tuple[str, ...],
|
80
|
+
include_path_regex: str | None,
|
81
|
+
include_method_regex: str | None,
|
82
|
+
include_name_regex: str | None,
|
83
|
+
include_tag_regex: str | None,
|
84
|
+
include_operation_id_regex: str | None,
|
85
|
+
exclude_path: tuple[str, ...],
|
86
|
+
exclude_method: tuple[str, ...],
|
87
|
+
exclude_name: tuple[str, ...],
|
88
|
+
exclude_tag: tuple[str, ...],
|
89
|
+
exclude_operation_id: tuple[str, ...],
|
90
|
+
exclude_path_regex: str | None,
|
91
|
+
exclude_method_regex: str | None,
|
92
|
+
exclude_name_regex: str | None,
|
93
|
+
exclude_tag_regex: str | None,
|
94
|
+
exclude_operation_id_regex: str | None,
|
95
|
+
include_by: Callable | None,
|
96
|
+
exclude_by: Callable | None,
|
97
|
+
exclude_deprecated: bool,
|
98
|
+
) -> FilterSet:
|
99
|
+
# Build explicit include filters
|
100
|
+
include_set = FilterSet()
|
101
|
+
if include_by:
|
102
|
+
include_set.include(include_by)
|
103
|
+
for name_ in include_name:
|
104
|
+
include_set.include(name=name_)
|
105
|
+
for method in include_method:
|
106
|
+
include_set.include(method=method)
|
107
|
+
for path in include_path:
|
108
|
+
include_set.include(path=path)
|
109
|
+
for tag in include_tag:
|
110
|
+
include_set.include(tag=tag)
|
111
|
+
for operation_id in include_operation_id:
|
112
|
+
include_set.include(operation_id=operation_id)
|
113
|
+
if (
|
114
|
+
include_name_regex
|
115
|
+
or include_method_regex
|
116
|
+
or include_path_regex
|
117
|
+
or include_tag_regex
|
118
|
+
or include_operation_id_regex
|
119
|
+
):
|
120
|
+
include_set.include(
|
121
|
+
name_regex=include_name_regex,
|
122
|
+
method_regex=include_method_regex,
|
123
|
+
path_regex=include_path_regex,
|
124
|
+
tag_regex=include_tag_regex,
|
125
|
+
operation_id_regex=include_operation_id_regex,
|
126
|
+
)
|
127
|
+
|
128
|
+
# Build explicit exclude filters
|
129
|
+
exclude_set = FilterSet()
|
130
|
+
if exclude_by:
|
131
|
+
exclude_set.include(exclude_by)
|
132
|
+
for name_ in exclude_name:
|
133
|
+
exclude_set.include(name=name_)
|
134
|
+
for method in exclude_method:
|
135
|
+
exclude_set.include(method=method)
|
136
|
+
for path in exclude_path:
|
137
|
+
exclude_set.include(path=path)
|
138
|
+
for tag in exclude_tag:
|
139
|
+
exclude_set.include(tag=tag)
|
140
|
+
for operation_id in exclude_operation_id:
|
141
|
+
exclude_set.include(operation_id=operation_id)
|
142
|
+
if (
|
143
|
+
exclude_name_regex
|
144
|
+
or exclude_method_regex
|
145
|
+
or exclude_path_regex
|
146
|
+
or exclude_tag_regex
|
147
|
+
or exclude_operation_id_regex
|
148
|
+
):
|
149
|
+
exclude_set.include(
|
150
|
+
name_regex=exclude_name_regex,
|
151
|
+
method_regex=exclude_method_regex,
|
152
|
+
path_regex=exclude_path_regex,
|
153
|
+
tag_regex=exclude_tag_regex,
|
154
|
+
operation_id_regex=exclude_operation_id_regex,
|
155
|
+
)
|
156
|
+
|
157
|
+
# Add deprecated operations to exclude filters if requested
|
158
|
+
if exclude_deprecated:
|
159
|
+
exclude_set.include(is_deprecated)
|
160
|
+
|
161
|
+
# Also update operations list for consistency with config structure
|
162
|
+
if not include_set.is_empty():
|
163
|
+
self.operations.insert(0, OperationConfig(filter_set=include_set, enabled=True))
|
164
|
+
if not exclude_set.is_empty():
|
165
|
+
self.operations.insert(0, OperationConfig(filter_set=exclude_set, enabled=False))
|
166
|
+
|
167
|
+
final = FilterSet()
|
168
|
+
|
169
|
+
# Get a stable reference to operations
|
170
|
+
operations = list(self.operations)
|
171
|
+
|
172
|
+
# Define a closure that implements our priority logic
|
173
|
+
def priority_filter(ctx: HasAPIOperation) -> bool:
|
174
|
+
"""Filter operations according to CLI and config priority."""
|
175
|
+
# 1. CLI includes override everything if present
|
176
|
+
if not include_set.is_empty():
|
177
|
+
return include_set.match(ctx)
|
178
|
+
|
179
|
+
# 2. CLI excludes take precedence over config
|
180
|
+
if not exclude_set.is_empty() and exclude_set.match(ctx):
|
181
|
+
return False
|
182
|
+
|
183
|
+
# 3. Check config operations in priority order (first match wins)
|
184
|
+
for op_config in operations:
|
185
|
+
if op_config._filter_set.match(ctx):
|
186
|
+
return op_config.enabled
|
187
|
+
|
188
|
+
# 4. Default to include if no rule matches
|
189
|
+
return True
|
190
|
+
|
191
|
+
# Add our priority function as the filter
|
192
|
+
final.include(priority_filter)
|
193
|
+
|
194
|
+
return final
|
195
|
+
|
196
|
+
|
197
|
+
@dataclass
|
198
|
+
class OperationConfig(DiffBase):
|
199
|
+
_filter_set: FilterSet
|
200
|
+
enabled: bool
|
201
|
+
headers: dict | None
|
202
|
+
proxy: str | None
|
203
|
+
continue_on_failure: bool | None
|
204
|
+
tls_verify: bool | str | None
|
205
|
+
rate_limit: Limiter | None
|
206
|
+
request_timeout: float | int | None
|
207
|
+
request_cert: str | None
|
208
|
+
request_cert_key: str | None
|
209
|
+
parameters: dict[str, Any]
|
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
|
+
"request_timeout",
|
225
|
+
"request_cert",
|
226
|
+
"request_cert_key",
|
227
|
+
"parameters",
|
228
|
+
"auth",
|
229
|
+
"checks",
|
230
|
+
"phases",
|
231
|
+
"generation",
|
232
|
+
)
|
233
|
+
|
234
|
+
def __init__(
|
235
|
+
self,
|
236
|
+
*,
|
237
|
+
filter_set: FilterSet | None = None,
|
238
|
+
enabled: bool = True,
|
239
|
+
headers: dict | None = None,
|
240
|
+
proxy: str | None = None,
|
241
|
+
continue_on_failure: bool | None = None,
|
242
|
+
tls_verify: bool | str | None = None,
|
243
|
+
rate_limit: str | None = None,
|
244
|
+
request_timeout: float | int | None = None,
|
245
|
+
request_cert: str | None = None,
|
246
|
+
request_cert_key: str | None = None,
|
247
|
+
parameters: dict[str, Any] | None = None,
|
248
|
+
auth: AuthConfig | None = None,
|
249
|
+
checks: ChecksConfig | None = None,
|
250
|
+
phases: PhasesConfig | None = None,
|
251
|
+
generation: GenerationConfig | None = None,
|
252
|
+
) -> None:
|
253
|
+
self._filter_set = filter_set or FilterSet()
|
254
|
+
self.enabled = enabled
|
255
|
+
self.headers = headers
|
256
|
+
self.proxy = proxy
|
257
|
+
self.continue_on_failure = continue_on_failure
|
258
|
+
self.tls_verify = tls_verify
|
259
|
+
if rate_limit is not None:
|
260
|
+
self.rate_limit = build_limiter(rate_limit)
|
261
|
+
else:
|
262
|
+
self.rate_limit = rate_limit
|
263
|
+
self._rate_limit = rate_limit
|
264
|
+
self.request_timeout = request_timeout
|
265
|
+
self.request_cert = request_cert
|
266
|
+
self.request_cert_key = request_cert_key
|
267
|
+
self.parameters = parameters or {}
|
268
|
+
self.auth = auth or AuthConfig()
|
269
|
+
self.checks = checks or ChecksConfig()
|
270
|
+
self.phases = phases or PhasesConfig()
|
271
|
+
self.generation = generation or GenerationConfig()
|
272
|
+
|
273
|
+
@classmethod
|
274
|
+
def from_dict(cls, data: dict[str, Any]) -> OperationConfig:
|
275
|
+
filter_set = FilterSet()
|
276
|
+
for key_suffix, arg_suffix in (("", ""), ("-regex", "_regex")):
|
277
|
+
for attr, arg_name in FILTER_ATTRIBUTES:
|
278
|
+
key = f"include-{attr}{key_suffix}"
|
279
|
+
if key in data:
|
280
|
+
with reraise_filter_error(attr):
|
281
|
+
filter_set.include(**{f"{arg_name}{arg_suffix}": data[key]})
|
282
|
+
key = f"exclude-{attr}{key_suffix}"
|
283
|
+
if key in data:
|
284
|
+
with reraise_filter_error(attr):
|
285
|
+
filter_set.exclude(**{f"{arg_name}{arg_suffix}": data[key]})
|
286
|
+
for key, method in (("include-by", filter_set.include), ("exclude-by", filter_set.exclude)):
|
287
|
+
if key in data:
|
288
|
+
expression = data[key]
|
289
|
+
try:
|
290
|
+
func = expression_to_filter_function(expression)
|
291
|
+
method(func)
|
292
|
+
except ValueError:
|
293
|
+
raise ConfigError(f"Invalid filter expression: '{expression}'") from None
|
294
|
+
|
295
|
+
return cls(
|
296
|
+
filter_set=filter_set,
|
297
|
+
enabled=data.get("enabled", True),
|
298
|
+
headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
|
299
|
+
if "headers" in data
|
300
|
+
else None,
|
301
|
+
proxy=resolve(data.get("proxy")),
|
302
|
+
continue_on_failure=data.get("continue-on-failure", None),
|
303
|
+
tls_verify=resolve(data.get("tls-verify")),
|
304
|
+
rate_limit=resolve(data.get("rate-limit")),
|
305
|
+
request_timeout=data.get("request-timeout"),
|
306
|
+
request_cert=resolve(data.get("request-cert")),
|
307
|
+
request_cert_key=resolve(data.get("request-cert-key")),
|
308
|
+
parameters=load_parameters(data),
|
309
|
+
auth=AuthConfig.from_dict(data.get("auth", {})),
|
310
|
+
checks=ChecksConfig.from_dict(data.get("checks", {})),
|
311
|
+
phases=PhasesConfig.from_dict(data.get("phases", {})),
|
312
|
+
generation=GenerationConfig.from_dict(data.get("generation", {})),
|
313
|
+
)
|