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.
- schemathesis/__init__.py +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +189 -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 +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +523 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- 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/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- 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 +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -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/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import sys
|
4
|
-
from dataclasses import dataclass
|
5
4
|
from typing import Any, Callable
|
6
5
|
|
7
6
|
import click
|
@@ -13,15 +12,13 @@ from schemathesis.cli.commands.run.handlers.base import EventHandler
|
|
13
12
|
from schemathesis.cli.commands.run.handlers.cassettes import CassetteWriter
|
14
13
|
from schemathesis.cli.commands.run.handlers.junitxml import JunitXMLHandler
|
15
14
|
from schemathesis.cli.commands.run.handlers.output import OutputHandler
|
16
|
-
from schemathesis.cli.commands.run.loaders import
|
17
|
-
from schemathesis.cli.commands.run.reports import ReportConfig, ReportFormat
|
15
|
+
from schemathesis.cli.commands.run.loaders import load_schema
|
18
16
|
from schemathesis.cli.ext.fs import open_file
|
17
|
+
from schemathesis.config import ProjectConfig, ReportFormat
|
19
18
|
from schemathesis.core.errors import LoaderError
|
20
|
-
from schemathesis.core.
|
19
|
+
from schemathesis.core.fs import file_exists
|
21
20
|
from schemathesis.engine import from_schema
|
22
|
-
from schemathesis.engine.config import EngineConfig
|
23
21
|
from schemathesis.engine.events import EventGenerator, FatalError, Interrupted
|
24
|
-
from schemathesis.filters import FilterSet
|
25
22
|
|
26
23
|
CUSTOM_HANDLERS: list[type[EventHandler]] = []
|
27
24
|
|
@@ -35,41 +32,34 @@ def handler() -> Callable[[type], None]:
|
|
35
32
|
return _wrapper
|
36
33
|
|
37
34
|
|
38
|
-
|
39
|
-
|
40
|
-
location: str
|
41
|
-
|
42
|
-
filter_set:
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
def into_event_stream(config: RunConfig) -> EventGenerator:
|
58
|
-
loader_config = AutodetectConfig(
|
59
|
-
location=config.location,
|
60
|
-
network=config.engine.network,
|
61
|
-
wait_for_schema=config.wait_for_schema,
|
62
|
-
base_url=config.base_url,
|
63
|
-
rate_limit=config.rate_limit,
|
64
|
-
output=config.output,
|
65
|
-
generation=config.engine.execution.generation,
|
66
|
-
)
|
67
|
-
loading_started = LoadingStarted(location=config.location)
|
35
|
+
def execute(
|
36
|
+
*,
|
37
|
+
location: str,
|
38
|
+
config: ProjectConfig,
|
39
|
+
filter_set: dict[str, Any],
|
40
|
+
args: list[str],
|
41
|
+
params: dict[str, Any],
|
42
|
+
) -> None:
|
43
|
+
event_stream = into_event_stream(location=location, config=config, filter_set=filter_set)
|
44
|
+
_execute(event_stream, config=config, args=args, params=params)
|
45
|
+
|
46
|
+
|
47
|
+
MISSING_BASE_URL_MESSAGE = "The `--url` option is required when specifying a schema via a file."
|
48
|
+
|
49
|
+
|
50
|
+
def into_event_stream(*, location: str, config: ProjectConfig, filter_set: dict[str, Any]) -> EventGenerator:
|
51
|
+
# The whole engine idea is that it communicates with the outside via events, so handlers can react to them
|
52
|
+
# For this reason, even schema loading is done via a separate set of events.
|
53
|
+
loading_started = LoadingStarted(location=location)
|
68
54
|
yield loading_started
|
69
55
|
|
70
56
|
try:
|
71
|
-
schema = load_schema(
|
72
|
-
|
57
|
+
schema = load_schema(location=location, config=config)
|
58
|
+
# Schemas don't (yet?) use configs for deciding what operations should be tested, so
|
59
|
+
# a separate FilterSet passed there. It combines both config file filters + CLI options
|
60
|
+
schema.filter_set = schema.config.operations.create_filter_set(**filter_set)
|
61
|
+
if file_exists(location) and schema.config.base_url is None:
|
62
|
+
raise click.UsageError(MISSING_BASE_URL_MESSAGE)
|
73
63
|
except KeyboardInterrupt:
|
74
64
|
yield Interrupted(phase=None)
|
75
65
|
return
|
@@ -78,73 +68,75 @@ def into_event_stream(config: RunConfig) -> EventGenerator:
|
|
78
68
|
return
|
79
69
|
|
80
70
|
yield LoadingFinished(
|
81
|
-
location=
|
71
|
+
location=location,
|
82
72
|
start_time=loading_started.timestamp,
|
83
73
|
base_url=schema.get_base_url(),
|
84
74
|
specification=schema.specification,
|
85
75
|
statistic=schema.statistic,
|
86
76
|
schema=schema.raw_schema,
|
77
|
+
config=schema.config,
|
87
78
|
base_path=schema.base_path,
|
79
|
+
find_operation_by_label=schema.find_operation_by_label,
|
88
80
|
)
|
89
81
|
|
90
82
|
try:
|
91
|
-
yield from from_schema(schema
|
83
|
+
yield from from_schema(schema).execute()
|
92
84
|
except Exception as exc:
|
93
85
|
yield FatalError(exception=exc)
|
94
86
|
|
95
87
|
|
96
|
-
def initialize_handlers(
|
88
|
+
def initialize_handlers(
|
89
|
+
*,
|
90
|
+
config: ProjectConfig,
|
91
|
+
args: list[str],
|
92
|
+
params: dict[str, Any],
|
93
|
+
) -> list[EventHandler]:
|
97
94
|
"""Create event handlers based on run configuration."""
|
98
95
|
handlers: list[EventHandler] = []
|
99
96
|
|
100
|
-
if config.
|
101
|
-
|
102
|
-
|
97
|
+
if config.reports.junit.enabled:
|
98
|
+
path = config.reports.get_path(ReportFormat.JUNIT)
|
99
|
+
open_file(path)
|
100
|
+
handlers.append(JunitXMLHandler(path))
|
101
|
+
for format, report in (
|
102
|
+
(ReportFormat.VCR, config.reports.vcr),
|
103
|
+
(ReportFormat.HAR, config.reports.har),
|
104
|
+
):
|
105
|
+
if report.enabled:
|
106
|
+
path = config.reports.get_path(format)
|
103
107
|
open_file(path)
|
104
|
-
handlers.append(
|
105
|
-
|
106
|
-
for format in (ReportFormat.VCR, ReportFormat.HAR):
|
107
|
-
if format in config.report.formats:
|
108
|
-
path = config.report.get_path(format)
|
109
|
-
open_file(path)
|
110
|
-
handlers.append(
|
111
|
-
CassetteWriter(
|
112
|
-
format=format,
|
113
|
-
path=path,
|
114
|
-
sanitize_output=config.report.sanitize_output,
|
115
|
-
preserve_bytes=config.report.preserve_bytes,
|
116
|
-
)
|
117
|
-
)
|
108
|
+
handlers.append(CassetteWriter(format=format, path=path, config=config))
|
118
109
|
|
119
110
|
for custom_handler in CUSTOM_HANDLERS:
|
120
|
-
handlers.append(custom_handler(*
|
121
|
-
|
122
|
-
handlers.append(
|
123
|
-
OutputHandler(
|
124
|
-
workers_num=config.engine.execution.workers_num,
|
125
|
-
seed=config.engine.execution.seed,
|
126
|
-
rate_limit=config.rate_limit,
|
127
|
-
wait_for_schema=config.wait_for_schema,
|
128
|
-
engine_config=config.engine,
|
129
|
-
report_config=config.report,
|
130
|
-
)
|
131
|
-
)
|
111
|
+
handlers.append(custom_handler(*args, **params))
|
112
|
+
|
113
|
+
handlers.append(OutputHandler(config=config))
|
132
114
|
|
133
115
|
return handlers
|
134
116
|
|
135
117
|
|
136
|
-
def _execute(
|
137
|
-
|
138
|
-
|
118
|
+
def _execute(
|
119
|
+
event_stream: EventGenerator,
|
120
|
+
*,
|
121
|
+
config: ProjectConfig,
|
122
|
+
args: list[str],
|
123
|
+
params: dict[str, Any],
|
124
|
+
) -> None:
|
125
|
+
handlers: list[EventHandler] = []
|
126
|
+
ctx: ExecutionContext | None = None
|
139
127
|
|
140
128
|
def shutdown() -> None:
|
141
|
-
|
142
|
-
_handler
|
143
|
-
|
144
|
-
for handler in handlers:
|
145
|
-
handler.start(ctx)
|
129
|
+
if ctx is not None:
|
130
|
+
for _handler in handlers:
|
131
|
+
_handler.shutdown(ctx)
|
146
132
|
|
147
133
|
try:
|
134
|
+
handlers = initialize_handlers(config=config, args=args, params=params)
|
135
|
+
ctx = ExecutionContext(config=config)
|
136
|
+
|
137
|
+
for handler in handlers:
|
138
|
+
handler.start(ctx)
|
139
|
+
|
148
140
|
for event in event_stream:
|
149
141
|
ctx.on_event(event)
|
150
142
|
for handler in handlers:
|
@@ -1,13 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
from typing import
|
3
|
+
from functools import partial
|
4
|
+
from typing import Callable, Literal
|
5
5
|
|
6
6
|
import click
|
7
7
|
|
8
8
|
from schemathesis.cli.ext.groups import grouped_option
|
9
|
-
from schemathesis.core.errors import IncorrectUsage
|
10
|
-
from schemathesis.filters import FilterSet, expression_to_filter_function, is_deprecated
|
11
9
|
|
12
10
|
|
13
11
|
def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Literal["regex"] | None) -> Callable:
|
@@ -18,19 +16,32 @@ def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Lite
|
|
18
16
|
"operation-id": "ID",
|
19
17
|
"name": "Operation name",
|
20
18
|
}.get(by, by.capitalize())
|
19
|
+
callback = None
|
21
20
|
if modifier:
|
22
21
|
param += f"-{modifier}"
|
23
22
|
prop += " pattern"
|
23
|
+
else:
|
24
|
+
callback = partial(validate_filter, arg_name=param)
|
24
25
|
help_text = f"{prop} to {action} testing."
|
25
26
|
return grouped_option(
|
26
27
|
param,
|
27
28
|
help=help_text,
|
28
29
|
type=str,
|
29
30
|
multiple=modifier is None,
|
31
|
+
callback=callback,
|
30
32
|
hidden=True,
|
31
33
|
)
|
32
34
|
|
33
35
|
|
36
|
+
def validate_filter(
|
37
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: list[str], arg_name: str
|
38
|
+
) -> list[str]:
|
39
|
+
if len(raw_value) != len(set(raw_value)):
|
40
|
+
duplicates = ",".join(sorted({value for value in raw_value if raw_value.count(value) > 1}))
|
41
|
+
raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
|
42
|
+
return raw_value
|
43
|
+
|
44
|
+
|
34
45
|
_BY_VALUES = ("operation-id", "tag", "name", "method", "path")
|
35
46
|
|
36
47
|
|
@@ -40,164 +51,3 @@ def with_filters(command: Callable) -> Callable:
|
|
40
51
|
for modifier in ("regex", None):
|
41
52
|
command = _with_filter(by=by, mode=mode, modifier=modifier)(command) # type: ignore[arg-type]
|
42
53
|
return command
|
43
|
-
|
44
|
-
|
45
|
-
@dataclass
|
46
|
-
class FilterArguments:
|
47
|
-
include_path: Sequence[str]
|
48
|
-
include_method: Sequence[str]
|
49
|
-
include_name: Sequence[str]
|
50
|
-
include_tag: Sequence[str]
|
51
|
-
include_operation_id: Sequence[str]
|
52
|
-
include_path_regex: str | None
|
53
|
-
include_method_regex: str | None
|
54
|
-
include_name_regex: str | None
|
55
|
-
include_tag_regex: str | None
|
56
|
-
include_operation_id_regex: str | None
|
57
|
-
|
58
|
-
exclude_path: Sequence[str]
|
59
|
-
exclude_method: Sequence[str]
|
60
|
-
exclude_name: Sequence[str]
|
61
|
-
exclude_tag: Sequence[str]
|
62
|
-
exclude_operation_id: Sequence[str]
|
63
|
-
exclude_path_regex: str | None
|
64
|
-
exclude_method_regex: str | None
|
65
|
-
exclude_name_regex: str | None
|
66
|
-
exclude_tag_regex: str | None
|
67
|
-
exclude_operation_id_regex: str | None
|
68
|
-
|
69
|
-
include_by: str | None
|
70
|
-
exclude_by: str | None
|
71
|
-
exclude_deprecated: bool
|
72
|
-
|
73
|
-
__slots__ = (
|
74
|
-
"include_path",
|
75
|
-
"include_method",
|
76
|
-
"include_name",
|
77
|
-
"include_tag",
|
78
|
-
"include_operation_id",
|
79
|
-
"include_path_regex",
|
80
|
-
"include_method_regex",
|
81
|
-
"include_name_regex",
|
82
|
-
"include_tag_regex",
|
83
|
-
"include_operation_id_regex",
|
84
|
-
"exclude_path",
|
85
|
-
"exclude_method",
|
86
|
-
"exclude_name",
|
87
|
-
"exclude_tag",
|
88
|
-
"exclude_operation_id",
|
89
|
-
"exclude_path_regex",
|
90
|
-
"exclude_method_regex",
|
91
|
-
"exclude_name_regex",
|
92
|
-
"exclude_tag_regex",
|
93
|
-
"exclude_operation_id_regex",
|
94
|
-
"include_by",
|
95
|
-
"exclude_by",
|
96
|
-
"exclude_deprecated",
|
97
|
-
)
|
98
|
-
|
99
|
-
def into(self) -> FilterSet:
|
100
|
-
# Validate unique filter arguments
|
101
|
-
for values, arg_name in (
|
102
|
-
(self.include_path, "--include-path"),
|
103
|
-
(self.include_method, "--include-method"),
|
104
|
-
(self.include_name, "--include-name"),
|
105
|
-
(self.include_tag, "--include-tag"),
|
106
|
-
(self.include_operation_id, "--include-operation-id"),
|
107
|
-
(self.exclude_path, "--exclude-path"),
|
108
|
-
(self.exclude_method, "--exclude-method"),
|
109
|
-
(self.exclude_name, "--exclude-name"),
|
110
|
-
(self.exclude_tag, "--exclude-tag"),
|
111
|
-
(self.exclude_operation_id, "--exclude-operation-id"),
|
112
|
-
):
|
113
|
-
validate_unique_filter(values, arg_name)
|
114
|
-
|
115
|
-
# Convert include/exclude expressions to functions
|
116
|
-
include_by_function = _filter_by_expression_to_func(self.include_by, "--include-by")
|
117
|
-
exclude_by_function = _filter_by_expression_to_func(self.exclude_by, "--exclude-by")
|
118
|
-
|
119
|
-
filter_set = FilterSet()
|
120
|
-
|
121
|
-
# Apply include filters
|
122
|
-
if include_by_function:
|
123
|
-
filter_set.include(include_by_function)
|
124
|
-
for name_ in self.include_name:
|
125
|
-
filter_set.include(name=name_)
|
126
|
-
for method in self.include_method:
|
127
|
-
filter_set.include(method=method)
|
128
|
-
for path in self.include_path:
|
129
|
-
filter_set.include(path=path)
|
130
|
-
for tag in self.include_tag:
|
131
|
-
filter_set.include(tag=tag)
|
132
|
-
for operation_id in self.include_operation_id:
|
133
|
-
filter_set.include(operation_id=operation_id)
|
134
|
-
if (
|
135
|
-
self.include_name_regex
|
136
|
-
or self.include_method_regex
|
137
|
-
or self.include_path_regex
|
138
|
-
or self.include_tag_regex
|
139
|
-
or self.include_operation_id_regex
|
140
|
-
):
|
141
|
-
filter_set.include(
|
142
|
-
name_regex=self.include_name_regex,
|
143
|
-
method_regex=self.include_method_regex,
|
144
|
-
path_regex=self.include_path_regex,
|
145
|
-
tag_regex=self.include_tag_regex,
|
146
|
-
operation_id_regex=self.include_operation_id_regex,
|
147
|
-
)
|
148
|
-
|
149
|
-
# Apply exclude filters
|
150
|
-
if exclude_by_function:
|
151
|
-
filter_set.exclude(exclude_by_function)
|
152
|
-
for name_ in self.exclude_name:
|
153
|
-
apply_exclude_filter(filter_set, "name", name=name_)
|
154
|
-
for method in self.exclude_method:
|
155
|
-
apply_exclude_filter(filter_set, "method", method=method)
|
156
|
-
for path in self.exclude_path:
|
157
|
-
apply_exclude_filter(filter_set, "path", path=path)
|
158
|
-
for tag in self.exclude_tag:
|
159
|
-
apply_exclude_filter(filter_set, "tag", tag=tag)
|
160
|
-
for operation_id in self.exclude_operation_id:
|
161
|
-
apply_exclude_filter(filter_set, "operation-id", operation_id=operation_id)
|
162
|
-
for key, value, name in (
|
163
|
-
("name_regex", self.exclude_name_regex, "name-regex"),
|
164
|
-
("method_regex", self.exclude_method_regex, "method-regex"),
|
165
|
-
("path_regex", self.exclude_path_regex, "path-regex"),
|
166
|
-
("tag_regex", self.exclude_tag_regex, "tag-regex"),
|
167
|
-
("operation_id_regex", self.exclude_operation_id_regex, "operation-id-regex"),
|
168
|
-
):
|
169
|
-
if value:
|
170
|
-
apply_exclude_filter(filter_set, name, **{key: value})
|
171
|
-
|
172
|
-
# Exclude deprecated operations
|
173
|
-
if self.exclude_deprecated:
|
174
|
-
filter_set.exclude(is_deprecated)
|
175
|
-
|
176
|
-
return filter_set
|
177
|
-
|
178
|
-
|
179
|
-
def apply_exclude_filter(filter_set: FilterSet, option_name: str, **kwargs: Any) -> None:
|
180
|
-
"""Apply an exclude filter with proper error handling."""
|
181
|
-
try:
|
182
|
-
filter_set.exclude(**kwargs)
|
183
|
-
except IncorrectUsage as e:
|
184
|
-
if str(e) == "Filter already exists":
|
185
|
-
raise click.UsageError(
|
186
|
-
f"Filter for {option_name} already exists. You can't simultaneously include and exclude the same thing."
|
187
|
-
) from None
|
188
|
-
raise click.UsageError(str(e)) from None
|
189
|
-
|
190
|
-
|
191
|
-
def validate_unique_filter(values: Sequence[str], arg_name: str) -> None:
|
192
|
-
if len(values) != len(set(values)):
|
193
|
-
duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
|
194
|
-
raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
|
195
|
-
|
196
|
-
|
197
|
-
def _filter_by_expression_to_func(value: str | None, arg_name: str) -> Callable | None:
|
198
|
-
if value:
|
199
|
-
try:
|
200
|
-
return expression_to_filter_function(value)
|
201
|
-
except ValueError:
|
202
|
-
raise click.UsageError(f"Invalid expression for {arg_name}: {value}") from None
|
203
|
-
return None
|