schemathesis 4.0.13__py3-none-any.whl → 4.0.15__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/cli/commands/__init__.py +9 -0
- schemathesis/cli/commands/run/__init__.py +27 -26
- schemathesis/cli/commands/run/validation.py +13 -3
- schemathesis/config/_generation.py +8 -8
- schemathesis/config/_projects.py +2 -2
- schemathesis/config/_report.py +1 -1
- schemathesis/generation/coverage.py +126 -16
- schemathesis/generation/hypothesis/builder.py +16 -9
- schemathesis/specs/openapi/schemas.py +1 -1
- {schemathesis-4.0.13.dist-info → schemathesis-4.0.15.dist-info}/METADATA +1 -1
- {schemathesis-4.0.13.dist-info → schemathesis-4.0.15.dist-info}/RECORD +14 -14
- {schemathesis-4.0.13.dist-info → schemathesis-4.0.15.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.13.dist-info → schemathesis-4.0.15.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.13.dist-info → schemathesis-4.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -36,6 +36,15 @@ def schemathesis(ctx: click.Context, config_file: str | None) -> None:
|
|
36
36
|
config = SchemathesisConfig.from_path(config_file)
|
37
37
|
else:
|
38
38
|
config = SchemathesisConfig.discover()
|
39
|
+
except FileNotFoundError:
|
40
|
+
display_header(SCHEMATHESIS_VERSION)
|
41
|
+
click.secho(
|
42
|
+
f"❌ Failed to load configuration file from {config_file}",
|
43
|
+
fg="red",
|
44
|
+
bold=True,
|
45
|
+
)
|
46
|
+
click.echo("\nThe configuration file does not exist")
|
47
|
+
ctx.exit(1)
|
39
48
|
except (TOMLDecodeError, ConfigError) as exc:
|
40
49
|
display_header(SCHEMATHESIS_VERSION)
|
41
50
|
click.secho(
|
@@ -9,7 +9,7 @@ from click.utils import LazyFile
|
|
9
9
|
from schemathesis.checks import CHECKS, load_all_checks
|
10
10
|
from schemathesis.cli.commands.run import executor, validation
|
11
11
|
from schemathesis.cli.commands.run.filters import with_filters
|
12
|
-
from schemathesis.cli.constants import
|
12
|
+
from schemathesis.cli.constants import MAX_WORKERS, MIN_WORKERS
|
13
13
|
from schemathesis.cli.core import ensure_color
|
14
14
|
from schemathesis.cli.ext.groups import group, grouped_option
|
15
15
|
from schemathesis.cli.ext.options import (
|
@@ -62,7 +62,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
62
62
|
["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
|
63
63
|
choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
|
64
64
|
),
|
65
|
-
default=
|
65
|
+
default=None,
|
66
66
|
show_default=True,
|
67
67
|
callback=validation.convert_workers,
|
68
68
|
metavar="",
|
@@ -172,7 +172,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
172
172
|
help="Skip deprecated operations",
|
173
173
|
is_flag=True,
|
174
174
|
is_eager=True,
|
175
|
-
default=
|
175
|
+
default=None,
|
176
176
|
show_default=True,
|
177
177
|
)
|
178
178
|
@group("Network requests options")
|
@@ -206,7 +206,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
206
206
|
"request_tls_verify",
|
207
207
|
help="Path to CA bundle for TLS verification, or 'false' to disable",
|
208
208
|
type=str,
|
209
|
-
default=
|
209
|
+
default=None,
|
210
210
|
show_default=True,
|
211
211
|
callback=validation.convert_boolean_string,
|
212
212
|
)
|
@@ -280,13 +280,13 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
280
280
|
help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
|
281
281
|
type=bool,
|
282
282
|
is_flag=True,
|
283
|
-
default=
|
283
|
+
default=None,
|
284
284
|
callback=validation.validate_preserve_bytes,
|
285
285
|
)
|
286
286
|
@grouped_option(
|
287
287
|
"--output-sanitize",
|
288
288
|
type=str,
|
289
|
-
default=
|
289
|
+
default=None,
|
290
290
|
show_default=True,
|
291
291
|
help="Enable or disable automatic output sanitization to obscure sensitive data",
|
292
292
|
metavar="BOOLEAN",
|
@@ -296,7 +296,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
296
296
|
"--output-truncate",
|
297
297
|
help="Truncate schemas and responses in error messages",
|
298
298
|
type=str,
|
299
|
-
default=
|
299
|
+
default=None,
|
300
300
|
show_default=True,
|
301
301
|
metavar="BOOLEAN",
|
302
302
|
callback=validation.convert_boolean_string,
|
@@ -331,20 +331,21 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
331
331
|
"generation_no_shrink",
|
332
332
|
help="Disable test case shrinking. Makes test failures harder to debug but improves performance",
|
333
333
|
is_flag=True,
|
334
|
+
default=None,
|
334
335
|
)
|
335
336
|
@grouped_option(
|
336
337
|
"--generation-deterministic",
|
337
338
|
help="Enables deterministic mode, which eliminates random variation between tests",
|
338
339
|
is_flag=True,
|
339
340
|
is_eager=True,
|
340
|
-
default=
|
341
|
+
default=None,
|
341
342
|
show_default=True,
|
342
343
|
)
|
343
344
|
@grouped_option(
|
344
345
|
"--generation-allow-x00",
|
345
346
|
help="Whether to allow the generation of 'NULL' bytes within strings",
|
346
347
|
type=str,
|
347
|
-
default=
|
348
|
+
default=None,
|
348
349
|
show_default=True,
|
349
350
|
metavar="BOOLEAN",
|
350
351
|
callback=validation.convert_boolean_string,
|
@@ -353,7 +354,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
353
354
|
"--generation-codec",
|
354
355
|
help="The codec used for generating strings",
|
355
356
|
type=str,
|
356
|
-
default=
|
357
|
+
default=None,
|
357
358
|
callback=validation.validate_generation_codec,
|
358
359
|
)
|
359
360
|
@grouped_option(
|
@@ -371,7 +372,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
371
372
|
"--generation-with-security-parameters",
|
372
373
|
help="Whether to generate security parameters",
|
373
374
|
type=str,
|
374
|
-
default=
|
375
|
+
default=None,
|
375
376
|
show_default=True,
|
376
377
|
callback=validation.convert_boolean_string,
|
377
378
|
metavar="BOOLEAN",
|
@@ -380,7 +381,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
380
381
|
"--generation-graphql-allow-null",
|
381
382
|
help="Whether to use `null` values for optional arguments in GraphQL queries",
|
382
383
|
type=str,
|
383
|
-
default=
|
384
|
+
default=None,
|
384
385
|
show_default=True,
|
385
386
|
callback=validation.convert_boolean_string,
|
386
387
|
metavar="BOOLEAN",
|
@@ -398,7 +399,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
398
399
|
"generation_unique_inputs",
|
399
400
|
help="Force the generation of unique test cases",
|
400
401
|
is_flag=True,
|
401
|
-
default=
|
402
|
+
default=None,
|
402
403
|
show_default=True,
|
403
404
|
metavar="BOOLEAN",
|
404
405
|
)
|
@@ -440,15 +441,15 @@ def run(
|
|
440
441
|
exclude_operation_id_regex: str | None,
|
441
442
|
include_by: Callable | None = None,
|
442
443
|
exclude_by: Callable | None = None,
|
443
|
-
exclude_deprecated: bool =
|
444
|
-
workers: int =
|
444
|
+
exclude_deprecated: bool | None = None,
|
445
|
+
workers: int | None = None,
|
445
446
|
base_url: str | None,
|
446
447
|
wait_for_schema: float | None = None,
|
447
448
|
suppress_health_check: list[HealthCheck] | None,
|
448
449
|
warnings: bool | list[SchemathesisWarning] | None,
|
449
450
|
rate_limit: str | None = None,
|
450
451
|
request_timeout: int | None = None,
|
451
|
-
request_tls_verify: bool =
|
452
|
+
request_tls_verify: bool | None = None,
|
452
453
|
request_cert: str | None = None,
|
453
454
|
request_cert_key: str | None = None,
|
454
455
|
request_proxy: str | None = None,
|
@@ -457,21 +458,21 @@ def run(
|
|
457
458
|
report_junit_path: LazyFile | None = None,
|
458
459
|
report_vcr_path: LazyFile | None = None,
|
459
460
|
report_har_path: LazyFile | None = None,
|
460
|
-
report_preserve_bytes: bool =
|
461
|
-
output_sanitize: bool =
|
462
|
-
output_truncate: bool =
|
461
|
+
report_preserve_bytes: bool | None = None,
|
462
|
+
output_sanitize: bool | None = None,
|
463
|
+
output_truncate: bool | None = None,
|
463
464
|
generation_modes: list[GenerationMode],
|
464
465
|
generation_seed: int | None = None,
|
465
466
|
generation_max_examples: int | None = None,
|
466
467
|
generation_maximize: list[MetricFunction] | None,
|
467
|
-
generation_deterministic: bool =
|
468
|
+
generation_deterministic: bool | None = None,
|
468
469
|
generation_database: str | None = None,
|
469
|
-
generation_unique_inputs: bool =
|
470
|
-
generation_allow_x00: bool =
|
471
|
-
generation_graphql_allow_null: bool =
|
472
|
-
generation_with_security_parameters: bool =
|
473
|
-
generation_codec: str =
|
474
|
-
generation_no_shrink: bool =
|
470
|
+
generation_unique_inputs: bool | None = None,
|
471
|
+
generation_allow_x00: bool | None = None,
|
472
|
+
generation_graphql_allow_null: bool | None = None,
|
473
|
+
generation_with_security_parameters: bool | None = None,
|
474
|
+
generation_codec: str | None = None,
|
475
|
+
generation_no_shrink: bool | None = None,
|
475
476
|
force_color: bool = False,
|
476
477
|
no_color: bool = False,
|
477
478
|
**__kwargs: Any,
|
@@ -73,7 +73,11 @@ def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_
|
|
73
73
|
return raw_value
|
74
74
|
|
75
75
|
|
76
|
-
def validate_generation_codec(
|
76
|
+
def validate_generation_codec(
|
77
|
+
ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
|
78
|
+
) -> str | None:
|
79
|
+
if raw_value is None:
|
80
|
+
return raw_value
|
77
81
|
try:
|
78
82
|
codecs.getencoder(raw_value)
|
79
83
|
except LookupError as exc:
|
@@ -214,7 +218,11 @@ def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter
|
|
214
218
|
return [GenerationMode(value)]
|
215
219
|
|
216
220
|
|
217
|
-
def convert_boolean_string(
|
221
|
+
def convert_boolean_string(
|
222
|
+
ctx: click.core.Context, param: click.core.Parameter, value: str | None
|
223
|
+
) -> str | bool | None:
|
224
|
+
if value is None:
|
225
|
+
return value
|
218
226
|
return string_to_boolean(value)
|
219
227
|
|
220
228
|
|
@@ -226,7 +234,9 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
|
|
226
234
|
raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
|
227
235
|
|
228
236
|
|
229
|
-
def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
|
237
|
+
def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str | None) -> int | None:
|
238
|
+
if value is None:
|
239
|
+
return value
|
230
240
|
if value == "auto":
|
231
241
|
return get_workers_count()
|
232
242
|
return int(value)
|
@@ -105,34 +105,34 @@ class GenerationConfig(DiffBase):
|
|
105
105
|
*,
|
106
106
|
modes: list[GenerationMode] | None = None,
|
107
107
|
max_examples: int | None = None,
|
108
|
-
no_shrink: bool =
|
108
|
+
no_shrink: bool | None = None,
|
109
109
|
deterministic: bool | None = None,
|
110
|
-
allow_x00: bool =
|
110
|
+
allow_x00: bool | None = None,
|
111
111
|
codec: str | None = None,
|
112
112
|
maximize: list[MetricFunction] | None = None,
|
113
113
|
with_security_parameters: bool | None = None,
|
114
|
-
graphql_allow_null: bool =
|
114
|
+
graphql_allow_null: bool | None = None,
|
115
115
|
database: str | None = None,
|
116
|
-
unique_inputs: bool =
|
116
|
+
unique_inputs: bool | None = None,
|
117
117
|
exclude_header_characters: str | None = None,
|
118
118
|
) -> None:
|
119
119
|
if modes is not None:
|
120
120
|
self.modes = modes
|
121
121
|
if max_examples is not None:
|
122
122
|
self.max_examples = max_examples
|
123
|
-
self.no_shrink = no_shrink
|
123
|
+
self.no_shrink = no_shrink or False
|
124
124
|
self.deterministic = deterministic or False
|
125
|
-
self.allow_x00 = allow_x00
|
125
|
+
self.allow_x00 = allow_x00 if allow_x00 is not None else True
|
126
126
|
if codec is not None:
|
127
127
|
self.codec = codec
|
128
128
|
if maximize is not None:
|
129
129
|
self.maximize = maximize
|
130
130
|
if with_security_parameters is not None:
|
131
131
|
self.with_security_parameters = with_security_parameters
|
132
|
-
self.graphql_allow_null = graphql_allow_null
|
132
|
+
self.graphql_allow_null = graphql_allow_null if graphql_allow_null is not None else True
|
133
133
|
if database is not None:
|
134
134
|
self.database = database
|
135
|
-
self.unique_inputs = unique_inputs
|
135
|
+
self.unique_inputs = unique_inputs or False
|
136
136
|
if exclude_header_characters is not None:
|
137
137
|
self.exclude_header_characters = exclude_header_characters
|
138
138
|
|
schemathesis/config/_projects.py
CHANGED
@@ -98,7 +98,7 @@ class ProjectConfig(DiffBase):
|
|
98
98
|
workers: int | Literal["auto"] = DEFAULT_WORKERS,
|
99
99
|
proxy: str | None = None,
|
100
100
|
continue_on_failure: bool | None = None,
|
101
|
-
tls_verify: bool | str
|
101
|
+
tls_verify: bool | str = True,
|
102
102
|
rate_limit: str | None = None,
|
103
103
|
request_timeout: float | int | None = None,
|
104
104
|
request_cert: str | None = None,
|
@@ -155,7 +155,7 @@ class ProjectConfig(DiffBase):
|
|
155
155
|
workers=data.get("workers", DEFAULT_WORKERS),
|
156
156
|
proxy=resolve(data.get("proxy")),
|
157
157
|
continue_on_failure=data.get("continue-on-failure", None),
|
158
|
-
tls_verify=resolve(data.get("tls-verify")),
|
158
|
+
tls_verify=resolve(data.get("tls-verify", True)),
|
159
159
|
rate_limit=resolve(data.get("rate-limit")),
|
160
160
|
request_timeout=data.get("request-timeout"),
|
161
161
|
request_cert=resolve(data.get("request-cert")),
|
schemathesis/config/_report.py
CHANGED
@@ -94,7 +94,7 @@ class ReportsConfig(DiffBase):
|
|
94
94
|
vcr_path: str | None = None,
|
95
95
|
har_path: str | None = None,
|
96
96
|
directory: Path = DEFAULT_REPORT_DIRECTORY,
|
97
|
-
preserve_bytes: bool =
|
97
|
+
preserve_bytes: bool | None = None,
|
98
98
|
) -> None:
|
99
99
|
formats = formats or []
|
100
100
|
if junit_path is not None or ReportFormat.JUNIT in formats:
|
@@ -8,8 +8,9 @@ from functools import lru_cache, partial
|
|
8
8
|
from itertools import combinations
|
9
9
|
from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
|
10
10
|
from typing import Any, Callable, Generator, Iterator, TypeVar, cast
|
11
|
+
from urllib.parse import quote_plus
|
11
12
|
|
12
|
-
import jsonschema
|
13
|
+
import jsonschema.protocols
|
13
14
|
from hypothesis import strategies as st
|
14
15
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
15
16
|
from hypothesis_jsonschema import from_schema
|
@@ -19,7 +20,7 @@ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING
|
|
19
20
|
from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
|
20
21
|
from schemathesis.core.compat import RefResolutionError
|
21
22
|
from schemathesis.core.transforms import deepclone
|
22
|
-
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
23
|
+
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
23
24
|
from schemathesis.generation import GenerationMode
|
24
25
|
from schemathesis.generation.hypothesis import examples
|
25
26
|
from schemathesis.openapi.generation.filters import is_invalid_path_parameter
|
@@ -34,7 +35,7 @@ def _replace_zero_with_nonzero(x: float) -> float:
|
|
34
35
|
|
35
36
|
|
36
37
|
def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
37
|
-
return st.lists(strategy, max_size=
|
38
|
+
return st.lists(strategy, max_size=2) | st.dictionaries(st.text(), strategy, max_size=2)
|
38
39
|
|
39
40
|
|
40
41
|
NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
|
@@ -42,10 +43,12 @@ NEGATIVE_MODE_MAX_ITEMS = 15
|
|
42
43
|
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
43
44
|
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
44
45
|
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
45
|
-
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
|
46
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(max_size=16),
|
47
|
+
json_recursive_strategy,
|
48
|
+
max_leaves=2,
|
46
49
|
)
|
47
|
-
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
|
48
|
-
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
|
50
|
+
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2, max_size=3)
|
51
|
+
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(max_size=16), JSON_STRATEGY, max_size=2)
|
49
52
|
|
50
53
|
|
51
54
|
STRATEGIES_FOR_TYPE = {
|
@@ -112,8 +115,9 @@ class CoverageContext:
|
|
112
115
|
is_required: bool
|
113
116
|
path: list[str | int]
|
114
117
|
custom_formats: dict[str, st.SearchStrategy]
|
118
|
+
validator_cls: type[jsonschema.protocols.Validator]
|
115
119
|
|
116
|
-
__slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats")
|
120
|
+
__slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats", "validator_cls")
|
117
121
|
|
118
122
|
def __init__(
|
119
123
|
self,
|
@@ -123,12 +127,14 @@ class CoverageContext:
|
|
123
127
|
is_required: bool,
|
124
128
|
path: list[str | int] | None = None,
|
125
129
|
custom_formats: dict[str, st.SearchStrategy],
|
130
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
126
131
|
) -> None:
|
127
132
|
self.location = location
|
128
133
|
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
129
134
|
self.is_required = is_required
|
130
135
|
self.path = path or []
|
131
136
|
self.custom_formats = custom_formats
|
137
|
+
self.validator_cls = validator_cls
|
132
138
|
|
133
139
|
@contextmanager
|
134
140
|
def at(self, key: str | int) -> Generator[None, None, None]:
|
@@ -149,6 +155,7 @@ class CoverageContext:
|
|
149
155
|
is_required=self.is_required,
|
150
156
|
path=self.path,
|
151
157
|
custom_formats=self.custom_formats,
|
158
|
+
validator_cls=self.validator_cls,
|
152
159
|
)
|
153
160
|
|
154
161
|
def with_negative(self) -> CoverageContext:
|
@@ -158,6 +165,7 @@ class CoverageContext:
|
|
158
165
|
is_required=self.is_required,
|
159
166
|
path=self.path,
|
160
167
|
custom_formats=self.custom_formats,
|
168
|
+
validator_cls=self.validator_cls,
|
161
169
|
)
|
162
170
|
|
163
171
|
def is_valid_for_location(self, value: Any) -> bool:
|
@@ -173,20 +181,21 @@ class CoverageContext:
|
|
173
181
|
if isinstance(value, list) and not self.is_required:
|
174
182
|
# Optional parameters should be present
|
175
183
|
return any(item not in [{}, []] for item in value)
|
176
|
-
if isinstance(value, dict) and not self.is_required:
|
177
|
-
return bool(value)
|
178
184
|
return True
|
179
185
|
|
186
|
+
def will_be_serialized_to_string(self) -> bool:
|
187
|
+
return self.location in ("query", "path", "header", "cookie")
|
188
|
+
|
180
189
|
def can_be_negated(self, schema: dict[str, Any]) -> bool:
|
181
190
|
# Path, query, header, and cookie parameters will be stringified anyway
|
182
191
|
# If there are no constraints, then anything will match the original schema after serialization
|
183
|
-
if self.
|
192
|
+
if self.will_be_serialized_to_string():
|
184
193
|
cleaned = {
|
185
194
|
k: v
|
186
195
|
for k, v in schema.items()
|
187
196
|
if not k.startswith("x-") and k not in ["description", "example", "examples"]
|
188
197
|
}
|
189
|
-
return cleaned
|
198
|
+
return cleaned not in [{}, {"type": "string"}]
|
190
199
|
return True
|
191
200
|
|
192
201
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
@@ -224,6 +233,9 @@ class CoverageContext:
|
|
224
233
|
return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
|
225
234
|
if "enum" in schema:
|
226
235
|
return cached_draw(st.sampled_from(schema["enum"]))
|
236
|
+
if keys == ["multipleOf", "type"] and schema["type"] in ("integer", "number"):
|
237
|
+
step = schema["multipleOf"]
|
238
|
+
return cached_draw(st.integers().map(step.__mul__))
|
227
239
|
if "pattern" in schema:
|
228
240
|
pattern = schema["pattern"]
|
229
241
|
try:
|
@@ -411,7 +423,7 @@ def cover_schema_iter(
|
|
411
423
|
for value_ in _negative_enum(ctx, [value], seen):
|
412
424
|
yield value_
|
413
425
|
elif key == "type":
|
414
|
-
yield from _negative_type(ctx, value, seen)
|
426
|
+
yield from _negative_type(ctx, value, seen, schema)
|
415
427
|
elif key == "properties":
|
416
428
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
417
429
|
yield from _negative_properties(ctx, template, value)
|
@@ -1038,7 +1050,7 @@ def _negative_multiple_of(
|
|
1038
1050
|
|
1039
1051
|
|
1040
1052
|
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
1041
|
-
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
1053
|
+
unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
|
1042
1054
|
yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
|
1043
1055
|
|
1044
1056
|
|
@@ -1086,22 +1098,120 @@ def _is_non_integer_float(x: float) -> bool:
|
|
1086
1098
|
return x != int(x)
|
1087
1099
|
|
1088
1100
|
|
1101
|
+
def is_valid_header_value(value: Any) -> bool:
|
1102
|
+
value = str(value)
|
1103
|
+
if not is_latin_1_encodable(value):
|
1104
|
+
return False
|
1105
|
+
if has_invalid_characters("A", value):
|
1106
|
+
return False
|
1107
|
+
return True
|
1108
|
+
|
1109
|
+
|
1110
|
+
def jsonify(value: Any) -> Any:
|
1111
|
+
if isinstance(value, bool):
|
1112
|
+
return "true" if value else "false"
|
1113
|
+
elif value is None:
|
1114
|
+
return "null"
|
1115
|
+
|
1116
|
+
stack: list = [value]
|
1117
|
+
while stack:
|
1118
|
+
item = stack.pop()
|
1119
|
+
if isinstance(item, dict):
|
1120
|
+
for key, sub_item in item.items():
|
1121
|
+
if isinstance(sub_item, bool):
|
1122
|
+
item[key] = "true" if sub_item else "false"
|
1123
|
+
elif sub_item is None:
|
1124
|
+
item[key] = "null"
|
1125
|
+
elif isinstance(sub_item, dict):
|
1126
|
+
stack.append(sub_item)
|
1127
|
+
elif isinstance(sub_item, list):
|
1128
|
+
stack.extend(item)
|
1129
|
+
elif isinstance(item, list):
|
1130
|
+
for idx, sub_item in enumerate(item):
|
1131
|
+
if isinstance(sub_item, bool):
|
1132
|
+
item[idx] = "true" if sub_item else "false"
|
1133
|
+
elif sub_item is None:
|
1134
|
+
item[idx] = "null"
|
1135
|
+
else:
|
1136
|
+
stack.extend(item)
|
1137
|
+
return value
|
1138
|
+
|
1139
|
+
|
1140
|
+
def quote_path_parameter(value: Any) -> str:
|
1141
|
+
if isinstance(value, str):
|
1142
|
+
if value == ".":
|
1143
|
+
return "%2E"
|
1144
|
+
elif value == "..":
|
1145
|
+
return "%2E%2E"
|
1146
|
+
else:
|
1147
|
+
return quote_plus(value)
|
1148
|
+
if isinstance(value, list):
|
1149
|
+
return ",".join(map(str, value))
|
1150
|
+
return str(value)
|
1151
|
+
|
1152
|
+
|
1089
1153
|
def _negative_type(
|
1090
|
-
ctx: CoverageContext,
|
1091
|
-
ty: str | list[str],
|
1092
|
-
seen: HashSet,
|
1154
|
+
ctx: CoverageContext, ty: str | list[str], seen: HashSet, schema: dict[str, Any]
|
1093
1155
|
) -> Generator[GeneratedValue, None, None]:
|
1094
1156
|
if isinstance(ty, str):
|
1095
1157
|
types = [ty]
|
1096
1158
|
else:
|
1097
1159
|
types = ty
|
1098
1160
|
strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
|
1161
|
+
|
1162
|
+
filter_func = {
|
1163
|
+
"path": lambda x: not is_invalid_path_parameter(x),
|
1164
|
+
"header": is_valid_header_value,
|
1165
|
+
"cookie": is_valid_header_value,
|
1166
|
+
"query": lambda x: not contains_unicode_surrogate_pair(x),
|
1167
|
+
}.get(ctx.location)
|
1168
|
+
|
1099
1169
|
if "number" in types:
|
1100
1170
|
del strategies["integer"]
|
1101
1171
|
if "integer" in types:
|
1102
1172
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1103
1173
|
if ctx.location == "query":
|
1104
1174
|
strategies.pop("object", None)
|
1175
|
+
if filter_func is not None:
|
1176
|
+
for ty, strategy in strategies.items():
|
1177
|
+
strategies[ty] = strategy.filter(filter_func)
|
1178
|
+
|
1179
|
+
pattern = schema.get("pattern")
|
1180
|
+
if pattern is not None:
|
1181
|
+
try:
|
1182
|
+
re.compile(pattern)
|
1183
|
+
except re.error:
|
1184
|
+
schema = schema.copy()
|
1185
|
+
del schema["pattern"]
|
1186
|
+
return
|
1187
|
+
|
1188
|
+
validator = ctx.validator_cls(
|
1189
|
+
schema,
|
1190
|
+
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
1191
|
+
)
|
1192
|
+
is_valid = validator.is_valid
|
1193
|
+
try:
|
1194
|
+
is_valid(None)
|
1195
|
+
apply_validation = True
|
1196
|
+
except Exception:
|
1197
|
+
# Schema is not correct and we can't validate the generated instances.
|
1198
|
+
# In such a scenario it is better to generate at least something with some chances to have a false
|
1199
|
+
# positive failure
|
1200
|
+
apply_validation = False
|
1201
|
+
|
1202
|
+
def _does_not_match_the_original_schema(value: Any) -> bool:
|
1203
|
+
return not is_valid(str(value))
|
1204
|
+
|
1205
|
+
if ctx.location == "path":
|
1206
|
+
for ty, strategy in strategies.items():
|
1207
|
+
strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
|
1208
|
+
elif ctx.location == "query":
|
1209
|
+
for ty, strategy in strategies.items():
|
1210
|
+
strategies[ty] = strategy.map(jsonify)
|
1211
|
+
|
1212
|
+
if apply_validation and ctx.will_be_serialized_to_string():
|
1213
|
+
for ty, strategy in strategies.items():
|
1214
|
+
strategies[ty] = strategy.filter(_does_not_match_the_original_schema)
|
1105
1215
|
for strategy in strategies.values():
|
1106
1216
|
value = ctx.generate_from(strategy)
|
1107
1217
|
if seen.insert(value) and ctx.is_valid_for_location(value):
|
@@ -22,6 +22,7 @@ from schemathesis.config import GenerationConfig, ProjectConfig
|
|
22
22
|
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
|
23
23
|
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
24
24
|
from schemathesis.core.marks import Mark
|
25
|
+
from schemathesis.core.transforms import deepclone
|
25
26
|
from schemathesis.core.transport import prepare_urlencoded
|
26
27
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
27
28
|
from schemathesis.generation import GenerationMode, coverage
|
@@ -397,7 +398,7 @@ class Template:
|
|
397
398
|
return output
|
398
399
|
|
399
400
|
def unmodified(self) -> TemplateValue:
|
400
|
-
kwargs = self._template
|
401
|
+
kwargs = deepclone(self._template)
|
401
402
|
kwargs = self._serialize(kwargs)
|
402
403
|
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
403
404
|
|
@@ -462,6 +463,7 @@ def _iter_coverage_cases(
|
|
462
463
|
from schemathesis.specs.openapi._hypothesis import _build_custom_formats
|
463
464
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
464
465
|
from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
|
466
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
465
467
|
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
466
468
|
|
467
469
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
@@ -476,6 +478,8 @@ def _iter_coverage_cases(
|
|
476
478
|
|
477
479
|
seen_negative = coverage.HashSet()
|
478
480
|
seen_positive = coverage.HashSet()
|
481
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
482
|
+
validator_cls = operation.schema.validator_cls
|
479
483
|
|
480
484
|
for parameter in operation.iter_parameters():
|
481
485
|
location = parameter.location
|
@@ -489,6 +493,7 @@ def _iter_coverage_cases(
|
|
489
493
|
generation_modes=generation_modes,
|
490
494
|
is_required=parameter.is_required,
|
491
495
|
custom_formats=custom_formats,
|
496
|
+
validator_cls=validator_cls,
|
492
497
|
),
|
493
498
|
schema,
|
494
499
|
)
|
@@ -513,6 +518,7 @@ def _iter_coverage_cases(
|
|
513
518
|
generation_modes=generation_modes,
|
514
519
|
is_required=body.is_required,
|
515
520
|
custom_formats=custom_formats,
|
521
|
+
validator_cls=validator_cls,
|
516
522
|
),
|
517
523
|
schema,
|
518
524
|
)
|
@@ -658,14 +664,14 @@ def _iter_coverage_cases(
|
|
658
664
|
name = parameter.name
|
659
665
|
location = parameter.location
|
660
666
|
container_name = LOCATION_TO_CONTAINER[location]
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
667
|
+
container = template.get(container_name, {})
|
668
|
+
data = template.with_container(
|
669
|
+
container_name=container_name,
|
670
|
+
value={k: v for k, v in container.items() if k != name},
|
671
|
+
generation_mode=GenerationMode.NEGATIVE,
|
672
|
+
)
|
673
|
+
|
674
|
+
if seen_negative.insert(data.kwargs):
|
669
675
|
yield operation.Case(
|
670
676
|
**data.kwargs,
|
671
677
|
_meta=CaseMetadata(
|
@@ -747,6 +753,7 @@ def _iter_coverage_cases(
|
|
747
753
|
generation_modes=[GenerationMode.NEGATIVE],
|
748
754
|
is_required=is_required,
|
749
755
|
custom_formats=custom_formats,
|
756
|
+
validator_cls=validator_cls,
|
750
757
|
),
|
751
758
|
subschema,
|
752
759
|
)
|
@@ -195,7 +195,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
195
195
|
self.resolver.push_scope(scope)
|
196
196
|
try:
|
197
197
|
for method, definition in path_item.items():
|
198
|
-
if method not in HTTP_METHODS:
|
198
|
+
if method not in HTTP_METHODS or not definition:
|
199
199
|
continue
|
200
200
|
statistic.operations.total += 1
|
201
201
|
is_selected = not should_skip(path, method, definition)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 4.0.
|
3
|
+
Version: 4.0.15
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
|
@@ -10,15 +10,15 @@ schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,
|
|
10
10
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
11
11
|
schemathesis/cli/constants.py,sha256=CVcQNHEiX-joAQmyuEVKWPOSxDHsOw_EXXZsEclzLuY,341
|
12
12
|
schemathesis/cli/core.py,sha256=ue7YUdVo3YvuzGL4s6i62NL6YqNDeVPBSnQ1znrvG2w,480
|
13
|
-
schemathesis/cli/commands/__init__.py,sha256=
|
13
|
+
schemathesis/cli/commands/__init__.py,sha256=DNzKEnXu7GjGSVe0244ZErmygUBA3nGSyVY6JP3ixD0,3740
|
14
14
|
schemathesis/cli/commands/data.py,sha256=_ALywjIeCZjuaoDQFy-Kj8RZkEGqXd-Y95O47h8Jszs,171
|
15
|
-
schemathesis/cli/commands/run/__init__.py,sha256=
|
15
|
+
schemathesis/cli/commands/run/__init__.py,sha256=F8KgDQwWRqbJxnAL9nHREphcgGW-Ghn-kbe4yAquadw,18686
|
16
16
|
schemathesis/cli/commands/run/context.py,sha256=taegOHWc_B-HDwiU1R9Oi4q57mdfLXc-B954QUj8t7A,7984
|
17
17
|
schemathesis/cli/commands/run/events.py,sha256=ew0TQOc9T2YBZynYWv95k9yfAk8-hGuZDLMxjT8EhvY,1595
|
18
18
|
schemathesis/cli/commands/run/executor.py,sha256=kFbZw583SZ-jqjv8goTp2yEJOpZ_bzecyTeZvdc6qTE,5327
|
19
19
|
schemathesis/cli/commands/run/filters.py,sha256=pzkNRcf5vLPSsMfnvt711GNzRSBK5iZIFjPA0fiH1N4,1701
|
20
20
|
schemathesis/cli/commands/run/loaders.py,sha256=6j0ez7wduAUYbUT28ELKxMf-dYEWr_67m_KIuTSyNGk,4358
|
21
|
-
schemathesis/cli/commands/run/validation.py,sha256=
|
21
|
+
schemathesis/cli/commands/run/validation.py,sha256=DQaMiBLN2tYT9hONvv8xnyPvNXZH768UlOdUxTd5kZs,9193
|
22
22
|
schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
|
23
23
|
schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
|
24
24
|
schemathesis/cli/commands/run/handlers/cassettes.py,sha256=rRD4byjp4HXCkJS-zx3jSIFOJsPq77ejPpYeyCtsEZs,19461
|
@@ -34,15 +34,15 @@ schemathesis/config/_checks.py,sha256=F0r16eSSiICvoiTUkNNOE2PH73EGd8bikoeZdME_3Y
|
|
34
34
|
schemathesis/config/_diff_base.py,sha256=-XqS6cTzZC5fplz8_2RSZHDMSAPJhBBIEP6H8wcgHmo,4221
|
35
35
|
schemathesis/config/_env.py,sha256=8XfIyrnGNQuCDnfG0lwmKRFbasRUjgeQGBAMupsmtOU,613
|
36
36
|
schemathesis/config/_error.py,sha256=TxuuqQ1olwJc7P7ssfxXb1dB_Xn5uVsoazjvYvRxrxA,5437
|
37
|
-
schemathesis/config/_generation.py,sha256=
|
37
|
+
schemathesis/config/_generation.py,sha256=giWs4z17z9nRe_9Z3mAZ3LEoyh4hkcJnlAA6LSy6iEo,5210
|
38
38
|
schemathesis/config/_health_check.py,sha256=zC9inla5ibMBlEy5WyM4_TME7ju_KH3Bwfo21RI3Gks,561
|
39
39
|
schemathesis/config/_operations.py,sha256=2M36b4MMoFtaaFpe9yG-aWRqh0Qm1dpdk5M0V23X2yA,12129
|
40
40
|
schemathesis/config/_output.py,sha256=3G9SOi-4oNcQPHeNRG3HggFCwvcKOW1kF28a9m0H-pU,4434
|
41
41
|
schemathesis/config/_parameters.py,sha256=i76Hwaf834fBAMmtKfKTl1SFCicJ-Y-5tZt5QNGW2fA,618
|
42
42
|
schemathesis/config/_phases.py,sha256=NFUhn-xzEQdNtgNVW1t51lMquXbjRNGR_QuiCRLCi28,6454
|
43
|
-
schemathesis/config/_projects.py,sha256=
|
43
|
+
schemathesis/config/_projects.py,sha256=_fbivneAqa2Y7sCX0T1CBSjo3CHPD1qLZcJYsYnWpQk,19486
|
44
44
|
schemathesis/config/_rate_limit.py,sha256=ekEW-jP_Ichk_O6hYpj-h2TTTKfp7Fm0nyFUbvlWcbA,456
|
45
|
-
schemathesis/config/_report.py,sha256=
|
45
|
+
schemathesis/config/_report.py,sha256=ZECDpaCY4WWHD5UbjvgZoSjLz-rlTvfd5Ivzdgzqf2I,3891
|
46
46
|
schemathesis/config/_validator.py,sha256=IcE8geFZ0ZwR18rkIRs25i7pTl7Z84XbjYGUB-mqReU,258
|
47
47
|
schemathesis/config/_warnings.py,sha256=sI0VZcTj3dOnphhBwYwU_KTagxr89HGWTtQ99HcY84k,772
|
48
48
|
schemathesis/config/schema.json,sha256=wC1qe9M_fXotfmlBOmW_FCTRw9K5YC814-PipMGKllE,18907
|
@@ -85,13 +85,13 @@ schemathesis/engine/phases/unit/_executor.py,sha256=9MmZoKSBVSPk0LWwN3PZ3iaO9nzp
|
|
85
85
|
schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
|
86
86
|
schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
|
87
87
|
schemathesis/generation/case.py,sha256=zwAwFQ-Fp7SOxCXYOQyAdwAtNwVJe63PdLpvqackFQY,12296
|
88
|
-
schemathesis/generation/coverage.py,sha256=
|
88
|
+
schemathesis/generation/coverage.py,sha256=jpl1GpAGZXyoO2aZLUZlIE0h_ygdHMnvUK14oBg51k4,53896
|
89
89
|
schemathesis/generation/meta.py,sha256=adkoMuCfzSjHJ9ZDocQn0GnVldSCkLL3eVR5A_jafwM,2552
|
90
90
|
schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
|
91
91
|
schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
|
92
92
|
schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
|
93
93
|
schemathesis/generation/hypothesis/__init__.py,sha256=SVwM-rx07jPZzms0idWYACgUtWAxh49HRuTnaQ__zf0,1549
|
94
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
94
|
+
schemathesis/generation/hypothesis/builder.py,sha256=ujPp9ByUfNYdxWyKoJlxE0h8pAVBpmNJwWIi9MnR968,34824
|
95
95
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
96
96
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
97
97
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
@@ -134,7 +134,7 @@ schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKR
|
|
134
134
|
schemathesis/specs/openapi/parameters.py,sha256=ifu_QQCMUzsUHQAkvsOvLuokns6CzesssmQ3Nd3zxII,14594
|
135
135
|
schemathesis/specs/openapi/patterns.py,sha256=cBj8W4wn7VLJd4nABaIH5f502-zBDiqljxLgPWUn-50,15609
|
136
136
|
schemathesis/specs/openapi/references.py,sha256=40YcDExPLR2B8EOwt-Csw-5MYFi2xj_DXf91J0Pc9d4,8855
|
137
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
137
|
+
schemathesis/specs/openapi/schemas.py,sha256=FXD6yLindLNtjiW5D_GllWxfMAigSKjpjl8_GbqJXxE,52557
|
138
138
|
schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
|
139
139
|
schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
|
140
140
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
@@ -157,8 +157,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
|
|
157
157
|
schemathesis/transport/requests.py,sha256=rziZTrZCVMAqgy6ldB8iTwhkpAsnjKSgK8hj5Sq3ThE,10656
|
158
158
|
schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
|
159
159
|
schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
|
160
|
-
schemathesis-4.0.
|
161
|
-
schemathesis-4.0.
|
162
|
-
schemathesis-4.0.
|
163
|
-
schemathesis-4.0.
|
164
|
-
schemathesis-4.0.
|
160
|
+
schemathesis-4.0.15.dist-info/METADATA,sha256=nQbpPuX0uQ1CRaGdgF7Sor6E9pqtPKFYNHraxONfRdM,8472
|
161
|
+
schemathesis-4.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
162
|
+
schemathesis-4.0.15.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
163
|
+
schemathesis-4.0.15.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
164
|
+
schemathesis-4.0.15.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|