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
@@ -2,24 +2,22 @@ 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.
|
18
|
-
from schemathesis.core import rate_limit, string_to_boolean
|
13
|
+
from schemathesis.cli.ext.options import CsvEnumChoice
|
14
|
+
from schemathesis.config import ReportFormat, SchemathesisWarning, get_workers_count
|
15
|
+
from schemathesis.core import errors, rate_limit, string_to_boolean
|
19
16
|
from schemathesis.core.fs import file_exists
|
20
|
-
from schemathesis.core.validation import
|
17
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
18
|
+
from schemathesis.filters import expression_to_filter_function
|
21
19
|
from schemathesis.generation import GenerationMode
|
22
|
-
from schemathesis.generation.
|
20
|
+
from schemathesis.generation.metrics import MetricFunction
|
23
21
|
|
24
22
|
INVALID_DERANDOMIZE_MESSAGE = (
|
25
23
|
"`--generation-deterministic` implies no database, so passing `--generation-database` too is invalid."
|
@@ -35,27 +33,24 @@ INVALID_BASE_URL_MESSAGE = (
|
|
35
33
|
"The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
|
36
34
|
"Make sure it is a properly formatted URL."
|
37
35
|
)
|
38
|
-
MISSING_BASE_URL_MESSAGE = "The `--url` option is required when specifying a schema via a file."
|
39
36
|
MISSING_REQUEST_CERT_MESSAGE = "The `--request-cert` option must be specified if `--request-cert-key` is used."
|
40
37
|
|
41
38
|
|
42
|
-
def
|
39
|
+
def validate_schema_location(ctx: click.core.Context, param: click.core.Parameter, location: str) -> str:
|
43
40
|
try:
|
44
|
-
netloc = urlparse(
|
41
|
+
netloc = urlparse(location).netloc
|
45
42
|
if netloc:
|
46
|
-
validate_url(
|
47
|
-
return
|
43
|
+
validate_url(location)
|
44
|
+
return location
|
48
45
|
except ValueError as exc:
|
49
46
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE) from exc
|
50
|
-
if "\x00" in
|
47
|
+
if "\x00" in location or not location:
|
51
48
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE)
|
52
|
-
exists = file_exists(
|
53
|
-
if exists or bool(pathlib.Path(
|
49
|
+
exists = file_exists(location)
|
50
|
+
if exists or bool(pathlib.Path(location).suffix):
|
54
51
|
if not exists:
|
55
52
|
raise click.UsageError(FILE_DOES_NOT_EXIST_MESSAGE)
|
56
|
-
|
57
|
-
raise click.UsageError(MISSING_BASE_URL_MESSAGE)
|
58
|
-
return None
|
53
|
+
return location
|
59
54
|
raise click.UsageError(INVALID_SCHEMA_MESSAGE)
|
60
55
|
|
61
56
|
|
@@ -122,82 +117,31 @@ def validate_auth(
|
|
122
117
|
return None
|
123
118
|
|
124
119
|
|
125
|
-
def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str]
|
120
|
+
def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str]) -> None:
|
126
121
|
auth_is_set = auth is not None
|
127
122
|
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:
|
123
|
+
if len([is_set for is_set in (auth_is_set, header_is_set) if is_set]) > 1:
|
130
124
|
message = "The "
|
131
125
|
used = []
|
132
126
|
if auth_is_set:
|
133
127
|
used.append("`--auth`")
|
134
128
|
if header_is_set:
|
135
129
|
used.append("`--header`")
|
136
|
-
if override_is_set:
|
137
|
-
used.append("`--set-header`")
|
138
130
|
message += " and ".join(used)
|
139
131
|
message += " options were both used to set the 'Authorization' header, which is not permitted."
|
140
132
|
raise click.BadParameter(message)
|
141
133
|
|
142
134
|
|
143
|
-
def
|
144
|
-
|
145
|
-
) ->
|
146
|
-
|
147
|
-
for raw in values:
|
135
|
+
def validate_filter_expression(
|
136
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
137
|
+
) -> Callable | None:
|
138
|
+
if raw_value:
|
148
139
|
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)
|
140
|
+
return expression_to_filter_function(raw_value)
|
141
|
+
except ValueError:
|
142
|
+
arg_name = param.opts[0]
|
143
|
+
raise click.UsageError(f"Invalid expression for {arg_name}: {raw_value}") from None
|
144
|
+
return None
|
201
145
|
|
202
146
|
|
203
147
|
def _validate_header(key: str, value: str, where: str) -> None:
|
@@ -225,15 +169,6 @@ def validate_headers(
|
|
225
169
|
return headers
|
226
170
|
|
227
171
|
|
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
172
|
def validate_request_cert_key(
|
238
173
|
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
239
174
|
) -> str | None:
|
@@ -256,73 +191,26 @@ def validate_preserve_bytes(ctx: click.core.Context, param: click.core.Parameter
|
|
256
191
|
return True
|
257
192
|
|
258
193
|
|
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
|
194
|
+
def reduce_list(
|
195
|
+
ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]] | None
|
283
196
|
) -> list[str] | None:
|
284
197
|
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()
|
198
|
+
return None
|
199
|
+
return reduce(operator.iadd, value, [])
|
299
200
|
|
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
201
|
|
311
|
-
|
312
|
-
|
202
|
+
def convert_maximize(
|
203
|
+
ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]
|
204
|
+
) -> list[MetricFunction]:
|
205
|
+
from schemathesis.generation.metrics import METRICS
|
313
206
|
|
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
|
207
|
+
names: list[str] = reduce(operator.iadd, value, [])
|
208
|
+
return METRICS.get_by_names(names)
|
321
209
|
|
322
210
|
|
323
211
|
def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter, value: str) -> list[GenerationMode]:
|
324
212
|
if value == "all":
|
325
|
-
return GenerationMode
|
213
|
+
return list(GenerationMode)
|
326
214
|
return [GenerationMode(value)]
|
327
215
|
|
328
216
|
|
@@ -338,19 +226,21 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
|
|
338
226
|
raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
|
339
227
|
|
340
228
|
|
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
229
|
def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
|
354
230
|
if value == "auto":
|
355
231
|
return get_workers_count()
|
356
232
|
return int(value)
|
233
|
+
|
234
|
+
|
235
|
+
WARNINGS_CHOICE = CsvEnumChoice(SchemathesisWarning)
|
236
|
+
|
237
|
+
|
238
|
+
def validate_warnings(
|
239
|
+
ctx: click.core.Context, param: click.core.Parameter, value: str | None
|
240
|
+
) -> bool | None | list[SchemathesisWarning]:
|
241
|
+
if value is None:
|
242
|
+
return None
|
243
|
+
boolean = string_to_boolean(value)
|
244
|
+
if isinstance(boolean, bool):
|
245
|
+
return boolean
|
246
|
+
return WARNINGS_CHOICE.convert(value, param, ctx) # type: ignore[return-value]
|
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)
|
@@ -0,0 +1,189 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from os import PathLike
|
6
|
+
from random import Random
|
7
|
+
|
8
|
+
import tomli
|
9
|
+
|
10
|
+
from schemathesis.config._checks import (
|
11
|
+
CheckConfig,
|
12
|
+
ChecksConfig,
|
13
|
+
NotAServerErrorConfig,
|
14
|
+
PositiveDataAcceptanceConfig,
|
15
|
+
SimpleCheckConfig,
|
16
|
+
)
|
17
|
+
from schemathesis.config._diff_base import DiffBase
|
18
|
+
from schemathesis.config._error import ConfigError
|
19
|
+
from schemathesis.config._generation import GenerationConfig
|
20
|
+
from schemathesis.config._health_check import HealthCheck
|
21
|
+
from schemathesis.config._output import OutputConfig, SanitizationConfig, TruncationConfig
|
22
|
+
from schemathesis.config._phases import CoveragePhaseConfig, PhaseConfig, PhasesConfig, StatefulPhaseConfig
|
23
|
+
from schemathesis.config._projects import ProjectConfig, ProjectsConfig, SchemathesisWarning, get_workers_count
|
24
|
+
from schemathesis.config._report import DEFAULT_REPORT_DIRECTORY, ReportConfig, ReportFormat, ReportsConfig
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
"SchemathesisConfig",
|
28
|
+
"ConfigError",
|
29
|
+
"HealthCheck",
|
30
|
+
"ReportConfig",
|
31
|
+
"ReportsConfig",
|
32
|
+
"ReportFormat",
|
33
|
+
"DEFAULT_REPORT_DIRECTORY",
|
34
|
+
"GenerationConfig",
|
35
|
+
"OutputConfig",
|
36
|
+
"SanitizationConfig",
|
37
|
+
"TruncationConfig",
|
38
|
+
"ChecksConfig",
|
39
|
+
"CheckConfig",
|
40
|
+
"NotAServerErrorConfig",
|
41
|
+
"PositiveDataAcceptanceConfig",
|
42
|
+
"SimpleCheckConfig",
|
43
|
+
"PhaseConfig",
|
44
|
+
"PhasesConfig",
|
45
|
+
"CoveragePhaseConfig",
|
46
|
+
"StatefulPhaseConfig",
|
47
|
+
"ProjectsConfig",
|
48
|
+
"ProjectConfig",
|
49
|
+
"get_workers_count",
|
50
|
+
"SchemathesisWarning",
|
51
|
+
]
|
52
|
+
|
53
|
+
|
54
|
+
@dataclass(repr=False)
|
55
|
+
class SchemathesisConfig(DiffBase):
|
56
|
+
color: bool | None
|
57
|
+
suppress_health_check: list[HealthCheck]
|
58
|
+
_seed: int | None
|
59
|
+
wait_for_schema: float | int | None
|
60
|
+
max_failures: int | None
|
61
|
+
reports: ReportsConfig
|
62
|
+
output: OutputConfig
|
63
|
+
projects: ProjectsConfig
|
64
|
+
|
65
|
+
__slots__ = (
|
66
|
+
"color",
|
67
|
+
"suppress_health_check",
|
68
|
+
"_seed",
|
69
|
+
"wait_for_schema",
|
70
|
+
"max_failures",
|
71
|
+
"reports",
|
72
|
+
"output",
|
73
|
+
"projects",
|
74
|
+
)
|
75
|
+
|
76
|
+
def __init__(
|
77
|
+
self,
|
78
|
+
*,
|
79
|
+
color: bool | None = None,
|
80
|
+
suppress_health_check: list[HealthCheck] | None = None,
|
81
|
+
seed: int | None = None,
|
82
|
+
wait_for_schema: float | int | None = None,
|
83
|
+
max_failures: int | None = None,
|
84
|
+
reports: ReportsConfig | None = None,
|
85
|
+
output: OutputConfig | None = None,
|
86
|
+
projects: ProjectsConfig | None = None,
|
87
|
+
):
|
88
|
+
self.color = color
|
89
|
+
self.suppress_health_check = suppress_health_check or []
|
90
|
+
self._seed = seed
|
91
|
+
self.wait_for_schema = wait_for_schema
|
92
|
+
self.max_failures = max_failures
|
93
|
+
self.reports = reports or ReportsConfig()
|
94
|
+
self.output = output or OutputConfig()
|
95
|
+
self.projects = projects or ProjectsConfig()
|
96
|
+
self.projects._set_parent(self)
|
97
|
+
|
98
|
+
@property
|
99
|
+
def seed(self) -> int:
|
100
|
+
if self._seed is None:
|
101
|
+
self._seed = Random().getrandbits(128)
|
102
|
+
return self._seed
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def discover(cls) -> SchemathesisConfig:
|
106
|
+
"""Discover the Schemathesis configuration file.
|
107
|
+
|
108
|
+
Search for 'schemathesis.toml' in the current directory and then in each parent directory,
|
109
|
+
stopping when a directory containing a '.git' folder is encountered or the filesystem root is reached.
|
110
|
+
If a config file is found, load it; otherwise, return a default configuration.
|
111
|
+
"""
|
112
|
+
current_dir = os.getcwd()
|
113
|
+
config_file = None
|
114
|
+
|
115
|
+
while True:
|
116
|
+
candidate = os.path.join(current_dir, "schemathesis.toml")
|
117
|
+
if os.path.isfile(candidate):
|
118
|
+
config_file = candidate
|
119
|
+
break
|
120
|
+
|
121
|
+
# Stop searching if we've reached a git repository root
|
122
|
+
git_dir = os.path.join(current_dir, ".git")
|
123
|
+
if os.path.isdir(git_dir):
|
124
|
+
break
|
125
|
+
|
126
|
+
# Stop if we've reached the filesystem root
|
127
|
+
parent = os.path.dirname(current_dir)
|
128
|
+
if parent == current_dir:
|
129
|
+
break
|
130
|
+
current_dir = parent
|
131
|
+
|
132
|
+
if config_file:
|
133
|
+
return cls.from_path(config_file)
|
134
|
+
return cls()
|
135
|
+
|
136
|
+
def update(
|
137
|
+
self,
|
138
|
+
*,
|
139
|
+
color: bool | None = None,
|
140
|
+
suppress_health_check: list[HealthCheck] | None = None,
|
141
|
+
seed: int | None = None,
|
142
|
+
wait_for_schema: float | int | None = None,
|
143
|
+
max_failures: int | None,
|
144
|
+
) -> None:
|
145
|
+
"""Set top-level configuration options."""
|
146
|
+
if color is not None:
|
147
|
+
self.color = color
|
148
|
+
if suppress_health_check is not None:
|
149
|
+
self.suppress_health_check = suppress_health_check
|
150
|
+
if seed is not None:
|
151
|
+
self._seed = seed
|
152
|
+
if wait_for_schema is not None:
|
153
|
+
self.wait_for_schema = wait_for_schema
|
154
|
+
if max_failures is not None:
|
155
|
+
self.max_failures = max_failures
|
156
|
+
|
157
|
+
@classmethod
|
158
|
+
def from_path(cls, path: PathLike | str) -> SchemathesisConfig:
|
159
|
+
"""Load configuration from a file path."""
|
160
|
+
with open(path, encoding="utf-8") as fd:
|
161
|
+
return cls.from_str(fd.read())
|
162
|
+
|
163
|
+
@classmethod
|
164
|
+
def from_str(cls, data: str) -> SchemathesisConfig:
|
165
|
+
"""Parse configuration from a string."""
|
166
|
+
parsed = tomli.loads(data)
|
167
|
+
return cls.from_dict(parsed)
|
168
|
+
|
169
|
+
@classmethod
|
170
|
+
def from_dict(cls, data: dict) -> SchemathesisConfig:
|
171
|
+
"""Create a config instance from a dictionary."""
|
172
|
+
from jsonschema.exceptions import ValidationError
|
173
|
+
|
174
|
+
from schemathesis.config._validator import CONFIG_VALIDATOR
|
175
|
+
|
176
|
+
try:
|
177
|
+
CONFIG_VALIDATOR.validate(data)
|
178
|
+
except ValidationError as exc:
|
179
|
+
raise ConfigError.from_validation_error(exc) from None
|
180
|
+
return cls(
|
181
|
+
color=data.get("color"),
|
182
|
+
suppress_health_check=[HealthCheck(name) for name in data.get("suppress-health-check", [])],
|
183
|
+
seed=data.get("seed"),
|
184
|
+
wait_for_schema=data.get("wait-for-schema"),
|
185
|
+
max_failures=data.get("max-failures"),
|
186
|
+
reports=ReportsConfig.from_dict(data.get("reports", {})),
|
187
|
+
output=OutputConfig.from_dict(data.get("output", {})),
|
188
|
+
projects=ProjectsConfig.from_dict(data),
|
189
|
+
)
|
@@ -0,0 +1,51 @@
|
|
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
|
+
from schemathesis.config._env import resolve
|
8
|
+
from schemathesis.config._error import ConfigError
|
9
|
+
from schemathesis.core.validation import is_latin_1_encodable
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass(repr=False)
|
13
|
+
class AuthConfig(DiffBase):
|
14
|
+
basic: tuple[str, str] | None
|
15
|
+
|
16
|
+
__slots__ = ("basic",)
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
*,
|
21
|
+
basic: dict[str, str] | None = None,
|
22
|
+
) -> None:
|
23
|
+
if basic is not None:
|
24
|
+
assert "username" in basic
|
25
|
+
username = resolve(basic["username"])
|
26
|
+
assert "password" in basic
|
27
|
+
password = resolve(basic["password"])
|
28
|
+
_validate_basic(username, password)
|
29
|
+
self.basic = (username, password)
|
30
|
+
else:
|
31
|
+
self.basic = None
|
32
|
+
|
33
|
+
def update(self, *, basic: tuple[str, str] | None = None) -> None:
|
34
|
+
if basic is not None:
|
35
|
+
_validate_basic(*basic)
|
36
|
+
self.basic = basic
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def from_dict(cls, data: dict[str, Any]) -> AuthConfig:
|
40
|
+
return cls(basic=data.get("basic"))
|
41
|
+
|
42
|
+
@property
|
43
|
+
def is_defined(self) -> bool:
|
44
|
+
return self.basic is not None
|
45
|
+
|
46
|
+
|
47
|
+
def _validate_basic(username: str, password: str) -> None:
|
48
|
+
if not is_latin_1_encodable(username):
|
49
|
+
raise ConfigError("Username should be latin-1 encodable.")
|
50
|
+
if not is_latin_1_encodable(password):
|
51
|
+
raise ConfigError("Password should be latin-1 encodable.")
|