schemathesis 4.0.0a10__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 +5 -4
- 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 +2 -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 +153 -123
- 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.0a10.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.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -6,42 +6,28 @@ supporting both GraphQL and OpenAPI specifications.
|
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
+
import os
|
9
10
|
import warnings
|
10
|
-
from dataclasses import dataclass
|
11
11
|
from typing import TYPE_CHECKING, Any, Callable
|
12
12
|
|
13
13
|
from schemathesis import graphql, openapi
|
14
|
-
from schemathesis.
|
14
|
+
from schemathesis.config import ProjectConfig
|
15
15
|
from schemathesis.core.errors import LoaderError, LoaderErrorKind
|
16
16
|
from schemathesis.core.fs import file_exists
|
17
|
-
from schemathesis.core.output import OutputConfig
|
18
|
-
from schemathesis.generation import GenerationConfig
|
19
17
|
|
20
18
|
if TYPE_CHECKING:
|
21
|
-
from schemathesis.engine.config import NetworkConfig
|
22
19
|
from schemathesis.schemas import BaseSchema
|
23
20
|
|
24
|
-
Loader = Callable[["
|
21
|
+
Loader = Callable[["ProjectConfig"], "BaseSchema"]
|
25
22
|
|
26
23
|
|
27
|
-
|
28
|
-
class AutodetectConfig:
|
29
|
-
location: str
|
30
|
-
network: NetworkConfig
|
31
|
-
wait_for_schema: float | None
|
32
|
-
base_url: str | None | NotSet = NOT_SET
|
33
|
-
rate_limit: str | None | NotSet = NOT_SET
|
34
|
-
generation: GenerationConfig | NotSet = NOT_SET
|
35
|
-
output: OutputConfig | NotSet = NOT_SET
|
36
|
-
|
37
|
-
|
38
|
-
def load_schema(config: AutodetectConfig) -> BaseSchema:
|
24
|
+
def load_schema(location: str, config: ProjectConfig) -> BaseSchema:
|
39
25
|
"""Load API schema automatically based on the provided configuration."""
|
40
|
-
if is_probably_graphql(
|
26
|
+
if is_probably_graphql(location):
|
41
27
|
# Try GraphQL first, then fallback to Open API
|
42
|
-
return _try_load_schema(config, graphql, openapi)
|
28
|
+
return _try_load_schema(location, config, graphql, openapi)
|
43
29
|
# Try Open API first, then fallback to GraphQL
|
44
|
-
return _try_load_schema(config, openapi, graphql)
|
30
|
+
return _try_load_schema(location, config, openapi, graphql)
|
45
31
|
|
46
32
|
|
47
33
|
def should_try_more(exc: LoaderError) -> bool:
|
@@ -60,27 +46,28 @@ def should_try_more(exc: LoaderError) -> bool:
|
|
60
46
|
)
|
61
47
|
|
62
48
|
|
63
|
-
def detect_loader(
|
49
|
+
def detect_loader(location: str, module: Any) -> Callable:
|
64
50
|
"""Detect API schema loader."""
|
65
|
-
if
|
66
|
-
|
67
|
-
|
68
|
-
return module.from_url # type: ignore
|
69
|
-
raise NotImplementedError
|
51
|
+
if file_exists(location):
|
52
|
+
return module.from_path # type: ignore
|
53
|
+
return module.from_url # type: ignore
|
70
54
|
|
71
55
|
|
72
|
-
def _try_load_schema(config:
|
56
|
+
def _try_load_schema(location: str, config: ProjectConfig, first_module: Any, second_module: Any) -> BaseSchema:
|
73
57
|
"""Try to load schema with fallback option."""
|
74
58
|
from urllib3.exceptions import InsecureRequestWarning
|
75
59
|
|
76
60
|
with warnings.catch_warnings():
|
77
61
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
78
62
|
try:
|
79
|
-
return _load_schema(config, first_module)
|
63
|
+
return _load_schema(location, config, first_module)
|
80
64
|
except LoaderError as exc:
|
65
|
+
# If this was the OpenAPI loader on an explicit OpenAPI file, don't fallback
|
66
|
+
if first_module is openapi and is_openapi_file(location):
|
67
|
+
raise exc
|
81
68
|
if should_try_more(exc):
|
82
69
|
try:
|
83
|
-
return _load_schema(config, second_module)
|
70
|
+
return _load_schema(location, config, second_module)
|
84
71
|
except Exception as second_exc:
|
85
72
|
if is_specific_exception(second_exc):
|
86
73
|
raise second_exc
|
@@ -88,26 +75,23 @@ def _try_load_schema(config: AutodetectConfig, first_module: Any, second_module:
|
|
88
75
|
raise exc
|
89
76
|
|
90
77
|
|
91
|
-
def _load_schema(config:
|
78
|
+
def _load_schema(location: str, config: ProjectConfig, module: Any) -> BaseSchema:
|
92
79
|
"""Unified schema loader for both GraphQL and OpenAPI."""
|
93
|
-
loader = detect_loader(
|
80
|
+
loader = detect_loader(location, module)
|
94
81
|
|
95
82
|
kwargs: dict = {}
|
96
83
|
if loader is module.from_url:
|
97
84
|
if config.wait_for_schema is not None:
|
98
85
|
kwargs["wait_for_schema"] = config.wait_for_schema
|
99
|
-
kwargs["verify"] = config.
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
output=config.output,
|
109
|
-
generation=config.generation,
|
110
|
-
)
|
86
|
+
kwargs["verify"] = config.tls_verify
|
87
|
+
request_cert = config.request_cert_for()
|
88
|
+
if request_cert:
|
89
|
+
kwargs["cert"] = request_cert
|
90
|
+
auth = config.auth_for()
|
91
|
+
if auth is not None:
|
92
|
+
kwargs["auth"] = auth
|
93
|
+
|
94
|
+
return loader(location, config=config._parent, **kwargs)
|
111
95
|
|
112
96
|
|
113
97
|
def is_specific_exception(exc: Exception) -> bool:
|
@@ -120,10 +104,11 @@ def is_specific_exception(exc: Exception) -> bool:
|
|
120
104
|
)
|
121
105
|
|
122
106
|
|
123
|
-
def is_probably_graphql(
|
107
|
+
def is_probably_graphql(location: str) -> bool:
|
124
108
|
"""Detect whether it is likely that the given location is a GraphQL endpoint."""
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
)
|
109
|
+
return location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
|
110
|
+
|
111
|
+
|
112
|
+
def is_openapi_file(location: str) -> bool:
|
113
|
+
name = os.path.basename(location).lower()
|
114
|
+
return any(name == f"{base}{ext}" for base in ("openapi", "swagger") for ext in (".json", ".yaml", ".yml"))
|
@@ -2,24 +2,21 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import codecs
|
4
4
|
import operator
|
5
|
-
import os
|
6
5
|
import pathlib
|
7
|
-
import re
|
8
6
|
from contextlib import contextmanager
|
9
|
-
from functools import
|
10
|
-
from typing import Callable, Generator
|
7
|
+
from functools import reduce
|
8
|
+
from typing import Callable, Generator
|
11
9
|
from urllib.parse import urlparse
|
12
10
|
|
13
11
|
import click
|
14
12
|
|
15
|
-
from schemathesis import
|
16
|
-
from schemathesis.
|
17
|
-
from schemathesis.cli.constants import DEFAULT_WORKERS
|
18
|
-
from schemathesis.core import rate_limit, string_to_boolean
|
13
|
+
from schemathesis.config import ReportFormat, get_workers_count
|
14
|
+
from schemathesis.core import errors, rate_limit, string_to_boolean
|
19
15
|
from schemathesis.core.fs import file_exists
|
20
|
-
from schemathesis.core.validation import
|
16
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
17
|
+
from schemathesis.filters import expression_to_filter_function
|
21
18
|
from schemathesis.generation import GenerationMode
|
22
|
-
from schemathesis.generation.
|
19
|
+
from schemathesis.generation.targets import TargetFunction
|
23
20
|
|
24
21
|
INVALID_DERANDOMIZE_MESSAGE = (
|
25
22
|
"`--generation-deterministic` implies no database, so passing `--generation-database` too is invalid."
|
@@ -35,27 +32,24 @@ INVALID_BASE_URL_MESSAGE = (
|
|
35
32
|
"The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
|
36
33
|
"Make sure it is a properly formatted URL."
|
37
34
|
)
|
38
|
-
MISSING_BASE_URL_MESSAGE = "The `--url` option is required when specifying a schema via a file."
|
39
35
|
MISSING_REQUEST_CERT_MESSAGE = "The `--request-cert` option must be specified if `--request-cert-key` is used."
|
40
36
|
|
41
37
|
|
42
|
-
def
|
38
|
+
def validate_schema_location(ctx: click.core.Context, param: click.core.Parameter, location: str) -> str:
|
43
39
|
try:
|
44
|
-
netloc = urlparse(
|
40
|
+
netloc = urlparse(location).netloc
|
45
41
|
if netloc:
|
46
|
-
validate_url(
|
47
|
-
return
|
42
|
+
validate_url(location)
|
43
|
+
return location
|
48
44
|
except ValueError as exc:
|
49
45
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE) from exc
|
50
|
-
if "\x00" in
|
46
|
+
if "\x00" in location or not location:
|
51
47
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE)
|
52
|
-
exists = file_exists(
|
53
|
-
if exists or bool(pathlib.Path(
|
48
|
+
exists = file_exists(location)
|
49
|
+
if exists or bool(pathlib.Path(location).suffix):
|
54
50
|
if not exists:
|
55
51
|
raise click.UsageError(FILE_DOES_NOT_EXIST_MESSAGE)
|
56
|
-
|
57
|
-
raise click.UsageError(MISSING_BASE_URL_MESSAGE)
|
58
|
-
return None
|
52
|
+
return location
|
59
53
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE)
|
60
54
|
|
61
55
|
|
@@ -122,82 +116,31 @@ def validate_auth(
|
|
122
116
|
return None
|
123
117
|
|
124
118
|
|
125
|
-
def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str]
|
119
|
+
def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str]) -> None:
|
126
120
|
auth_is_set = auth is not None
|
127
121
|
header_is_set = "authorization" in {header.lower() for header in headers}
|
128
|
-
|
129
|
-
if len([is_set for is_set in (auth_is_set, header_is_set, override_is_set) if is_set]) > 1:
|
122
|
+
if len([is_set for is_set in (auth_is_set, header_is_set) if is_set]) > 1:
|
130
123
|
message = "The "
|
131
124
|
used = []
|
132
125
|
if auth_is_set:
|
133
126
|
used.append("`--auth`")
|
134
127
|
if header_is_set:
|
135
128
|
used.append("`--header`")
|
136
|
-
if override_is_set:
|
137
|
-
used.append("`--set-header`")
|
138
129
|
message += " and ".join(used)
|
139
130
|
message += " options were both used to set the 'Authorization' header, which is not permitted."
|
140
131
|
raise click.BadParameter(message)
|
141
132
|
|
142
133
|
|
143
|
-
def
|
144
|
-
|
145
|
-
) ->
|
146
|
-
|
147
|
-
for raw in values:
|
134
|
+
def validate_filter_expression(
|
135
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
136
|
+
) -> Callable | None:
|
137
|
+
if raw_value:
|
148
138
|
try:
|
149
|
-
|
150
|
-
except ValueError
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
raise click.BadParameter(f"{name} parameter name should not be empty.")
|
155
|
-
if key in output:
|
156
|
-
raise click.BadParameter(f"{name} parameter {key} is specified multiple times.")
|
157
|
-
value = value.strip()
|
158
|
-
callback(key, value)
|
159
|
-
output[key] = value
|
160
|
-
return output
|
161
|
-
|
162
|
-
|
163
|
-
def validate_unique_filter(values: Sequence[str], arg_name: str) -> None:
|
164
|
-
if len(values) != len(set(values)):
|
165
|
-
duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
|
166
|
-
raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
|
167
|
-
|
168
|
-
|
169
|
-
def _validate_set_query(_: str, value: str) -> None:
|
170
|
-
if contains_unicode_surrogate_pair(value):
|
171
|
-
raise click.BadParameter("Query parameter value should not contain surrogates.")
|
172
|
-
|
173
|
-
|
174
|
-
def validate_set_query(
|
175
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
|
176
|
-
) -> dict[str, str]:
|
177
|
-
return _validate_and_build_multiple_options(raw_value, "Query", _validate_set_query)
|
178
|
-
|
179
|
-
|
180
|
-
def validate_set_header(
|
181
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
|
182
|
-
) -> dict[str, str]:
|
183
|
-
return _validate_and_build_multiple_options(raw_value, "Header", partial(_validate_header, where="Header"))
|
184
|
-
|
185
|
-
|
186
|
-
def validate_set_cookie(
|
187
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
|
188
|
-
) -> dict[str, str]:
|
189
|
-
return _validate_and_build_multiple_options(raw_value, "Cookie", partial(_validate_header, where="Cookie"))
|
190
|
-
|
191
|
-
|
192
|
-
def _validate_set_path(_: str, value: str) -> None:
|
193
|
-
if contains_unicode_surrogate_pair(value):
|
194
|
-
raise click.BadParameter("Path parameter value should not contain surrogates.")
|
195
|
-
|
196
|
-
|
197
|
-
def validate_set_path(
|
198
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
|
199
|
-
) -> dict[str, str]:
|
200
|
-
return _validate_and_build_multiple_options(raw_value, "Path", _validate_set_path)
|
139
|
+
return expression_to_filter_function(raw_value)
|
140
|
+
except ValueError:
|
141
|
+
arg_name = param.opts[0]
|
142
|
+
raise click.UsageError(f"Invalid expression for {arg_name}: {raw_value}") from None
|
143
|
+
return None
|
201
144
|
|
202
145
|
|
203
146
|
def _validate_header(key: str, value: str, where: str) -> None:
|
@@ -225,15 +168,6 @@ def validate_headers(
|
|
225
168
|
return headers
|
226
169
|
|
227
170
|
|
228
|
-
def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]) -> tuple[str, ...]:
|
229
|
-
for value in raw_value:
|
230
|
-
try:
|
231
|
-
re.compile(value)
|
232
|
-
except (re.error, OverflowError, RuntimeError) as exc:
|
233
|
-
raise click.BadParameter(f"Invalid regex: {exc.args[0]}.") # noqa: B904
|
234
|
-
return raw_value
|
235
|
-
|
236
|
-
|
237
171
|
def validate_request_cert_key(
|
238
172
|
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
239
173
|
) -> str | None:
|
@@ -256,68 +190,21 @@ def validate_preserve_bytes(ctx: click.core.Context, param: click.core.Parameter
|
|
256
190
|
return True
|
257
191
|
|
258
192
|
|
259
|
-
def
|
260
|
-
ctx: click.core.Context, param: click.core.Parameter, value: tuple[str
|
261
|
-
) -> list[experimental.Experiment]:
|
262
|
-
return [
|
263
|
-
feature
|
264
|
-
for feature in experimental.GLOBAL_EXPERIMENTS.available
|
265
|
-
if feature.label in value or feature.is_env_var_set
|
266
|
-
]
|
267
|
-
|
268
|
-
|
269
|
-
def reduce_list(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
|
270
|
-
return reduce(operator.iadd, value, [])
|
271
|
-
|
272
|
-
|
273
|
-
def convert_http_methods(
|
274
|
-
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
275
|
-
) -> set[str] | None:
|
276
|
-
if value is None:
|
277
|
-
return value
|
278
|
-
return {item.lower() for item in value}
|
279
|
-
|
280
|
-
|
281
|
-
def convert_status_codes(
|
282
|
-
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
193
|
+
def reduce_list(
|
194
|
+
ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]] | None
|
283
195
|
) -> list[str] | None:
|
284
196
|
if not value:
|
285
|
-
return
|
286
|
-
|
287
|
-
invalid = []
|
288
|
-
|
289
|
-
for code in value:
|
290
|
-
if len(code) != 3:
|
291
|
-
invalid.append(code)
|
292
|
-
continue
|
293
|
-
|
294
|
-
if code[0] not in {"1", "2", "3", "4", "5"}:
|
295
|
-
invalid.append(code)
|
296
|
-
continue
|
297
|
-
|
298
|
-
upper_code = code.upper()
|
197
|
+
return None
|
198
|
+
return reduce(operator.iadd, value, [])
|
299
199
|
|
300
|
-
if "X" in upper_code:
|
301
|
-
if (
|
302
|
-
upper_code[1:] == "XX"
|
303
|
-
or (upper_code[1] == "X" and upper_code[2].isdigit())
|
304
|
-
or (upper_code[1].isdigit() and upper_code[2] == "X")
|
305
|
-
):
|
306
|
-
continue
|
307
|
-
else:
|
308
|
-
invalid.append(code)
|
309
|
-
continue
|
310
200
|
|
311
|
-
|
312
|
-
|
201
|
+
def convert_maximize(
|
202
|
+
ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]
|
203
|
+
) -> list[TargetFunction]:
|
204
|
+
from schemathesis.generation.targets import TARGETS
|
313
205
|
|
314
|
-
|
315
|
-
|
316
|
-
f"Invalid status code(s): {', '.join(invalid)}. "
|
317
|
-
"Use valid 3-digit codes between 100 and 599, "
|
318
|
-
"or wildcards (e.g., 2XX, 2X0, 20X), where X is a wildcard digit."
|
319
|
-
)
|
320
|
-
return value
|
206
|
+
names: list[str] = reduce(operator.iadd, value, [])
|
207
|
+
return TARGETS.get_by_names(names)
|
321
208
|
|
322
209
|
|
323
210
|
def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter, value: str) -> list[GenerationMode]:
|
@@ -338,18 +225,6 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
|
|
338
225
|
raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
|
339
226
|
|
340
227
|
|
341
|
-
def get_workers_count() -> int:
|
342
|
-
"""Detect the number of available CPUs for the current process, if possible.
|
343
|
-
|
344
|
-
Use ``DEFAULT_WORKERS`` if not possible to detect.
|
345
|
-
"""
|
346
|
-
if hasattr(os, "sched_getaffinity"):
|
347
|
-
# In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
|
348
|
-
return len(os.sched_getaffinity(0))
|
349
|
-
# Number of CPUs in the system, or 1 if undetermined
|
350
|
-
return os.cpu_count() or DEFAULT_WORKERS
|
351
|
-
|
352
|
-
|
353
228
|
def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
|
354
229
|
if value == "auto":
|
355
230
|
return get_workers_count()
|
schemathesis/cli/core.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
import shutil
|
3
5
|
|
@@ -9,9 +11,9 @@ def get_terminal_width() -> int:
|
|
9
11
|
return shutil.get_terminal_size((80, 24)).columns
|
10
12
|
|
11
13
|
|
12
|
-
def ensure_color(ctx: click.Context,
|
13
|
-
if
|
14
|
+
def ensure_color(ctx: click.Context, color: bool | None) -> None:
|
15
|
+
if color:
|
14
16
|
ctx.color = True
|
15
|
-
elif
|
17
|
+
elif color is False or "NO_COLOR" in os.environ:
|
16
18
|
ctx.color = False
|
17
19
|
os.environ["NO_COLOR"] = "1"
|
schemathesis/cli/ext/fs.py
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
1
3
|
import click
|
2
4
|
|
3
5
|
from schemathesis.core.fs import ensure_parent
|
4
6
|
|
5
7
|
|
6
|
-
def open_file(file:
|
8
|
+
def open_file(file: Path) -> None:
|
7
9
|
try:
|
8
|
-
ensure_parent(file
|
10
|
+
ensure_parent(file, fail_silently=False)
|
9
11
|
except OSError as exc:
|
10
12
|
raise click.BadParameter(f"'{file.name}': {exc.strerror}") from exc
|
11
13
|
try:
|
12
|
-
file.open()
|
13
|
-
except
|
14
|
-
raise click.BadParameter(exc
|
14
|
+
file.open("w", encoding="utf-8")
|
15
|
+
except OSError as exc:
|
16
|
+
raise click.BadParameter(f"Could not open file {file.name}: {exc}") from exc
|
schemathesis/cli/ext/options.py
CHANGED
@@ -5,7 +5,6 @@ from typing import Any, NoReturn
|
|
5
5
|
|
6
6
|
import click
|
7
7
|
|
8
|
-
from schemathesis.core import NOT_SET, NotSet
|
9
8
|
from schemathesis.core.registries import Registry
|
10
9
|
|
11
10
|
|
@@ -64,13 +63,6 @@ class CsvEnumChoice(BaseCsvChoice):
|
|
64
63
|
self.fail_on_invalid_options(invalid_options, selected)
|
65
64
|
|
66
65
|
|
67
|
-
class CsvListChoice(click.ParamType):
|
68
|
-
def convert( # type: ignore[return]
|
69
|
-
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
70
|
-
) -> list[str]:
|
71
|
-
return [item for item in value.split(",") if item]
|
72
|
-
|
73
|
-
|
74
66
|
class RegistryChoice(BaseCsvChoice):
|
75
67
|
def __init__(self, registry: Registry, with_all: bool = False) -> None:
|
76
68
|
self.registry = registry
|
@@ -91,16 +83,3 @@ class RegistryChoice(BaseCsvChoice):
|
|
91
83
|
if not invalid_options and selected:
|
92
84
|
return selected
|
93
85
|
self.fail_on_invalid_options(invalid_options, selected)
|
94
|
-
|
95
|
-
|
96
|
-
class OptionalInt(click.types.IntRange):
|
97
|
-
def convert( # type: ignore
|
98
|
-
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
99
|
-
) -> int | NotSet:
|
100
|
-
if value.lower() == "none":
|
101
|
-
return NOT_SET
|
102
|
-
try:
|
103
|
-
int(value)
|
104
|
-
return super().convert(value, param, ctx)
|
105
|
-
except ValueError:
|
106
|
-
self.fail(f"{value} is not a valid integer or None.", param, ctx)
|