schemathesis 3.30.3__py3-none-any.whl → 3.31.0__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/_hypothesis.py +13 -3
- schemathesis/cli/__init__.py +27 -2
- schemathesis/cli/callbacks.py +5 -0
- schemathesis/cli/cassettes.py +150 -12
- schemathesis/exceptions.py +3 -1
- schemathesis/generation/__init__.py +2 -0
- schemathesis/models.py +14 -3
- schemathesis/runner/events.py +2 -0
- schemathesis/runner/impl/core.py +43 -0
- schemathesis/schemas.py +3 -1
- schemathesis/service/serialization.py +24 -1
- schemathesis/specs/graphql/schemas.py +3 -1
- schemathesis/specs/openapi/examples.py +4 -2
- schemathesis/specs/openapi/references.py +19 -0
- schemathesis/specs/openapi/schemas.py +27 -13
- schemathesis/specs/openapi/security.py +16 -3
- schemathesis/stateful/context.py +7 -2
- schemathesis/stateful/events.py +16 -4
- schemathesis/stateful/runner.py +6 -11
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/validation.py +24 -12
- {schemathesis-3.30.3.dist-info → schemathesis-3.31.0.dist-info}/METADATA +2 -1
- {schemathesis-3.30.3.dist-info → schemathesis-3.31.0.dist-info}/RECORD +26 -26
- {schemathesis-3.30.3.dist-info → schemathesis-3.31.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.30.3.dist-info → schemathesis-3.31.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.30.3.dist-info → schemathesis-3.31.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -98,7 +98,9 @@ def create_test(
|
|
|
98
98
|
existing_settings = remove_explain_phase(existing_settings)
|
|
99
99
|
wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
|
|
100
100
|
if Phase.explicit in existing_settings.phases:
|
|
101
|
-
wrapped_test = add_examples(
|
|
101
|
+
wrapped_test = add_examples(
|
|
102
|
+
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
|
103
|
+
)
|
|
102
104
|
return wrapped_test
|
|
103
105
|
|
|
104
106
|
|
|
@@ -138,12 +140,20 @@ def make_async_test(test: Callable) -> Callable:
|
|
|
138
140
|
return async_run
|
|
139
141
|
|
|
140
142
|
|
|
141
|
-
def add_examples(
|
|
143
|
+
def add_examples(
|
|
144
|
+
test: Callable,
|
|
145
|
+
operation: APIOperation,
|
|
146
|
+
hook_dispatcher: HookDispatcher | None = None,
|
|
147
|
+
as_strategy_kwargs: dict[str, Any] | None = None,
|
|
148
|
+
) -> Callable:
|
|
142
149
|
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
|
143
150
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
144
151
|
|
|
145
152
|
try:
|
|
146
|
-
examples: list[Case] = [
|
|
153
|
+
examples: list[Case] = [
|
|
154
|
+
get_single_example(strategy)
|
|
155
|
+
for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
|
|
156
|
+
]
|
|
147
157
|
except (
|
|
148
158
|
OperationSchemaError,
|
|
149
159
|
HypothesisRefResolutionError,
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -522,6 +522,13 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
522
522
|
type=click.File("w", encoding="utf-8"),
|
|
523
523
|
is_eager=True,
|
|
524
524
|
)
|
|
525
|
+
@click.option(
|
|
526
|
+
"--cassette-format",
|
|
527
|
+
help="Format of the saved cassettes.",
|
|
528
|
+
type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
|
|
529
|
+
default=cassettes.CassetteFormat.VCR.name.lower(),
|
|
530
|
+
callback=callbacks.convert_cassette_format,
|
|
531
|
+
)
|
|
525
532
|
@click.option(
|
|
526
533
|
"--cassette-preserve-exact-body-bytes",
|
|
527
534
|
help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
|
|
@@ -718,6 +725,14 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
718
725
|
default="utf-8",
|
|
719
726
|
callback=callbacks.validate_generation_codec,
|
|
720
727
|
)
|
|
728
|
+
@click.option(
|
|
729
|
+
"--generation-with-security-parameters",
|
|
730
|
+
help="Whether to generate security parameters.",
|
|
731
|
+
type=str,
|
|
732
|
+
default="true",
|
|
733
|
+
show_default=True,
|
|
734
|
+
callback=callbacks.convert_boolean_string,
|
|
735
|
+
)
|
|
721
736
|
@click.option(
|
|
722
737
|
"--schemathesis-io-token",
|
|
723
738
|
help="Schemathesis.io authentication token.",
|
|
@@ -783,6 +798,7 @@ def run(
|
|
|
783
798
|
show_trace: bool = False,
|
|
784
799
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
785
800
|
cassette_path: click.utils.LazyFile | None = None,
|
|
801
|
+
cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
|
|
786
802
|
cassette_preserve_exact_body_bytes: bool = False,
|
|
787
803
|
store_network_log: click.utils.LazyFile | None = None,
|
|
788
804
|
wait_for_schema: float | None = None,
|
|
@@ -810,6 +826,7 @@ def run(
|
|
|
810
826
|
no_color: bool = False,
|
|
811
827
|
report_value: str | None = None,
|
|
812
828
|
generation_allow_x00: bool = True,
|
|
829
|
+
generation_with_security_parameters: bool = True,
|
|
813
830
|
generation_codec: str = "utf-8",
|
|
814
831
|
schemathesis_io_token: str | None = None,
|
|
815
832
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
@@ -849,7 +866,11 @@ def run(
|
|
|
849
866
|
|
|
850
867
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
851
868
|
|
|
852
|
-
generation_config = generation.GenerationConfig(
|
|
869
|
+
generation_config = generation.GenerationConfig(
|
|
870
|
+
allow_x00=generation_allow_x00,
|
|
871
|
+
codec=generation_codec,
|
|
872
|
+
with_security_parameters=generation_with_security_parameters,
|
|
873
|
+
)
|
|
853
874
|
|
|
854
875
|
report: ReportToService | click.utils.LazyFile | None
|
|
855
876
|
if report_value is None:
|
|
@@ -1006,6 +1027,7 @@ def run(
|
|
|
1006
1027
|
wait_for_schema=wait_for_schema,
|
|
1007
1028
|
validate_schema=validate_schema,
|
|
1008
1029
|
cassette_path=cassette_path,
|
|
1030
|
+
cassette_format=cassette_format,
|
|
1009
1031
|
cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
|
|
1010
1032
|
junit_xml=junit_xml,
|
|
1011
1033
|
verbosity=verbosity,
|
|
@@ -1387,6 +1409,7 @@ def execute(
|
|
|
1387
1409
|
wait_for_schema: float | None,
|
|
1388
1410
|
validate_schema: bool,
|
|
1389
1411
|
cassette_path: click.utils.LazyFile | None,
|
|
1412
|
+
cassette_format: cassettes.CassetteFormat,
|
|
1390
1413
|
cassette_preserve_exact_body_bytes: bool,
|
|
1391
1414
|
junit_xml: click.utils.LazyFile | None,
|
|
1392
1415
|
verbosity: int,
|
|
@@ -1449,7 +1472,9 @@ def execute(
|
|
|
1449
1472
|
# This handler should be first to have logs writing completed when the output handler will display statistic
|
|
1450
1473
|
_open_file(cassette_path)
|
|
1451
1474
|
handlers.append(
|
|
1452
|
-
cassettes.CassetteWriter(
|
|
1475
|
+
cassettes.CassetteWriter(
|
|
1476
|
+
cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
|
|
1477
|
+
)
|
|
1453
1478
|
)
|
|
1454
1479
|
handlers.append(get_output_handler(workers_num))
|
|
1455
1480
|
if sanitize_output:
|
schemathesis/cli/callbacks.py
CHANGED
|
@@ -24,6 +24,7 @@ from ..service.hosts import get_temporary_hosts_file
|
|
|
24
24
|
from ..stateful import Stateful
|
|
25
25
|
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
26
26
|
from ..types import PathLike
|
|
27
|
+
from .cassettes import CassetteFormat
|
|
27
28
|
from .constants import DEFAULT_WORKERS
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
@@ -344,6 +345,10 @@ def convert_code_sample_style(ctx: click.core.Context, param: click.core.Paramet
|
|
|
344
345
|
return CodeSampleStyle.from_str(value)
|
|
345
346
|
|
|
346
347
|
|
|
348
|
+
def convert_cassette_format(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CassetteFormat:
|
|
349
|
+
return CassetteFormat.from_str(value)
|
|
350
|
+
|
|
351
|
+
|
|
347
352
|
def convert_data_generation_method(
|
|
348
353
|
ctx: click.core.Context, param: click.core.Parameter, value: str
|
|
349
354
|
) -> list[DataGenerationMethod]:
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import enum
|
|
4
5
|
import json
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import threading
|
|
8
9
|
from dataclasses import dataclass, field
|
|
10
|
+
from http.cookies import SimpleCookie
|
|
9
11
|
from queue import Queue
|
|
10
|
-
from typing import IO, TYPE_CHECKING, Any, Generator, Iterator, cast
|
|
12
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
|
|
13
|
+
from urllib.parse import parse_qsl, urlparse
|
|
14
|
+
|
|
15
|
+
import harfile
|
|
11
16
|
|
|
12
17
|
from ..constants import SCHEMATHESIS_VERSION
|
|
13
18
|
from ..runner import events
|
|
@@ -27,6 +32,23 @@ if TYPE_CHECKING:
|
|
|
27
32
|
WRITER_WORKER_JOIN_TIMEOUT = 1
|
|
28
33
|
|
|
29
34
|
|
|
35
|
+
class CassetteFormat(str, enum.Enum):
|
|
36
|
+
"""Type of the cassette."""
|
|
37
|
+
|
|
38
|
+
VCR = "vcr"
|
|
39
|
+
HAR = "har"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_str(cls, value: str) -> CassetteFormat:
|
|
43
|
+
try:
|
|
44
|
+
return cls[value.upper()]
|
|
45
|
+
except KeyError:
|
|
46
|
+
available_formats = ", ".join(cls)
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Invalid value for cassette format: {value}. Available formats: {available_formats}"
|
|
49
|
+
) from None
|
|
50
|
+
|
|
51
|
+
|
|
30
52
|
@dataclass
|
|
31
53
|
class CassetteWriter(EventHandler):
|
|
32
54
|
"""Write interactions in a YAML cassette.
|
|
@@ -36,26 +58,30 @@ class CassetteWriter(EventHandler):
|
|
|
36
58
|
"""
|
|
37
59
|
|
|
38
60
|
file_handle: click.utils.LazyFile
|
|
61
|
+
format: CassetteFormat
|
|
39
62
|
preserve_exact_body_bytes: bool
|
|
40
63
|
queue: Queue = field(default_factory=Queue)
|
|
41
64
|
worker: threading.Thread = field(init=False)
|
|
42
65
|
|
|
43
66
|
def __post_init__(self) -> None:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
kwargs = {
|
|
68
|
+
"file_handle": self.file_handle,
|
|
69
|
+
"queue": self.queue,
|
|
70
|
+
"preserve_exact_body_bytes": self.preserve_exact_body_bytes,
|
|
71
|
+
}
|
|
72
|
+
writer: Callable
|
|
73
|
+
if self.format == CassetteFormat.HAR:
|
|
74
|
+
writer = har_writer
|
|
75
|
+
else:
|
|
76
|
+
writer = vcr_writer
|
|
77
|
+
self.worker = threading.Thread(name="SchemathesisCassetteWriter", target=writer, kwargs=kwargs)
|
|
52
78
|
self.worker.start()
|
|
53
79
|
|
|
54
80
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
55
81
|
if isinstance(event, events.Initialized):
|
|
56
82
|
# In the beginning we write metadata and start `http_interactions` list
|
|
57
83
|
self.queue.put(Initialize())
|
|
58
|
-
|
|
84
|
+
elif isinstance(event, events.AfterExecution):
|
|
59
85
|
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
60
86
|
# instance is created before `seed` is generated on the hypothesis side
|
|
61
87
|
seed = cast(int, event.result.seed)
|
|
@@ -71,7 +97,19 @@ class CassetteWriter(EventHandler):
|
|
|
71
97
|
interactions=event.result.interactions,
|
|
72
98
|
)
|
|
73
99
|
)
|
|
74
|
-
|
|
100
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
101
|
+
seed = cast(int, event.result.seed)
|
|
102
|
+
self.queue.put(
|
|
103
|
+
Process(
|
|
104
|
+
seed=seed,
|
|
105
|
+
# Correlation ID is not used in stateful testing
|
|
106
|
+
correlation_id="",
|
|
107
|
+
thread_id=event.thread_id,
|
|
108
|
+
data_generation_method=event.data_generation_method[0],
|
|
109
|
+
interactions=event.result.interactions,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(event, events.Finished):
|
|
75
113
|
self.shutdown()
|
|
76
114
|
|
|
77
115
|
def shutdown(self) -> None:
|
|
@@ -112,7 +150,7 @@ def get_command_representation() -> str:
|
|
|
112
150
|
return f"st {args}"
|
|
113
151
|
|
|
114
152
|
|
|
115
|
-
def
|
|
153
|
+
def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
|
116
154
|
"""Write YAML to a file in an incremental manner.
|
|
117
155
|
|
|
118
156
|
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
|
@@ -278,6 +316,106 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
278
316
|
stream.write('"')
|
|
279
317
|
|
|
280
318
|
|
|
319
|
+
def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
|
320
|
+
if preserve_exact_body_bytes:
|
|
321
|
+
|
|
322
|
+
def get_body(body: str) -> str:
|
|
323
|
+
return body
|
|
324
|
+
else:
|
|
325
|
+
|
|
326
|
+
def get_body(body: str) -> str:
|
|
327
|
+
return base64.b64decode(body).decode("utf-8", errors="replace")
|
|
328
|
+
|
|
329
|
+
with harfile.open(file_handle) as har:
|
|
330
|
+
while True:
|
|
331
|
+
item = queue.get()
|
|
332
|
+
if isinstance(item, Process):
|
|
333
|
+
for interaction in item.interactions:
|
|
334
|
+
time = round(interaction.response.elapsed * 1000, 2)
|
|
335
|
+
content_type = interaction.response.headers.get("Content-Type", [""])[0]
|
|
336
|
+
content = harfile.Content(
|
|
337
|
+
size=interaction.response.body_size or 0,
|
|
338
|
+
mimeType=content_type,
|
|
339
|
+
text=get_body(interaction.response.body) if interaction.response.body is not None else None,
|
|
340
|
+
encoding="base64"
|
|
341
|
+
if interaction.response.body is not None and preserve_exact_body_bytes
|
|
342
|
+
else None,
|
|
343
|
+
)
|
|
344
|
+
http_version = f"HTTP/{interaction.response.http_version}"
|
|
345
|
+
query_params = urlparse(interaction.request.uri).query
|
|
346
|
+
if interaction.request.body is not None:
|
|
347
|
+
post_data = harfile.PostData(
|
|
348
|
+
mimeType=content_type,
|
|
349
|
+
text=get_body(interaction.request.body),
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
post_data = None
|
|
353
|
+
har.add_entry(
|
|
354
|
+
startedDateTime=interaction.recorded_at,
|
|
355
|
+
time=time,
|
|
356
|
+
request=harfile.Request(
|
|
357
|
+
method=interaction.request.method.upper(),
|
|
358
|
+
url=interaction.request.uri,
|
|
359
|
+
httpVersion=http_version,
|
|
360
|
+
headers=[
|
|
361
|
+
harfile.Record(name=name, value=values[0])
|
|
362
|
+
for name, values in interaction.request.headers.items()
|
|
363
|
+
],
|
|
364
|
+
queryString=[
|
|
365
|
+
harfile.Record(name=name, value=value)
|
|
366
|
+
for name, value in parse_qsl(query_params, keep_blank_values=True)
|
|
367
|
+
],
|
|
368
|
+
cookies=_extract_cookies(interaction.request.headers.get("Cookie", [])),
|
|
369
|
+
headersSize=_headers_size(interaction.request.headers),
|
|
370
|
+
bodySize=interaction.request.body_size or 0,
|
|
371
|
+
postData=post_data,
|
|
372
|
+
),
|
|
373
|
+
response=harfile.Response(
|
|
374
|
+
status=interaction.response.status_code,
|
|
375
|
+
httpVersion=http_version,
|
|
376
|
+
statusText=interaction.response.message,
|
|
377
|
+
headers=[
|
|
378
|
+
harfile.Record(name=name, value=values[0])
|
|
379
|
+
for name, values in interaction.response.headers.items()
|
|
380
|
+
],
|
|
381
|
+
cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
|
|
382
|
+
content=content,
|
|
383
|
+
headersSize=_headers_size(interaction.response.headers),
|
|
384
|
+
bodySize=interaction.response.body_size or 0,
|
|
385
|
+
redirectURL=interaction.response.headers.get("Location", [""])[0],
|
|
386
|
+
),
|
|
387
|
+
timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
|
|
388
|
+
)
|
|
389
|
+
elif isinstance(item, Finalize):
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _headers_size(headers: dict[str, list[str]]) -> int:
|
|
394
|
+
size = 0
|
|
395
|
+
for name, values in headers.items():
|
|
396
|
+
# 4 is for ": " and "\r\n"
|
|
397
|
+
size += len(name) + 4 + len(values[0])
|
|
398
|
+
return size
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _extract_cookies(headers: list[str]) -> list[harfile.Cookie]:
|
|
402
|
+
return [cookie for items in headers for item in items for cookie in _cookie_to_har(item)]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _cookie_to_har(cookie: str) -> Iterator[harfile.Cookie]:
|
|
406
|
+
parsed = SimpleCookie(cookie)
|
|
407
|
+
for name, data in parsed.items():
|
|
408
|
+
yield harfile.Cookie(
|
|
409
|
+
name=name,
|
|
410
|
+
value=data.value,
|
|
411
|
+
path=data["path"] or None,
|
|
412
|
+
domain=data["domain"] or None,
|
|
413
|
+
expires=data["expires"] or None,
|
|
414
|
+
httpOnly=data["httponly"] or None,
|
|
415
|
+
secure=data["secure"] or None,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
281
419
|
@dataclass
|
|
282
420
|
class Replayed:
|
|
283
421
|
interaction: dict[str, Any]
|
schemathesis/exceptions.py
CHANGED
|
@@ -222,9 +222,11 @@ class OperationSchemaError(Exception):
|
|
|
222
222
|
def from_reference_resolution_error(
|
|
223
223
|
cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
|
|
224
224
|
) -> OperationSchemaError:
|
|
225
|
+
notes = getattr(error, "__notes__", [])
|
|
226
|
+
# Some exceptions don't have the actual reference in them, hence we add it manually via notes
|
|
227
|
+
pointer = f"'{notes[0]}'"
|
|
225
228
|
message = "Unresolvable JSON pointer in the schema"
|
|
226
229
|
# Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
|
|
227
|
-
pointer = str(error).split(": ", 1)[-1]
|
|
228
230
|
message += f"\n\nError details:\n JSON pointer: {pointer}"
|
|
229
231
|
message += "\n This typically means that the schema is referencing a component that doesn't exist."
|
|
230
232
|
message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
|
|
@@ -77,5 +77,7 @@ class GenerationConfig:
|
|
|
77
77
|
allow_x00: bool = True
|
|
78
78
|
# Generate strings using the given codec
|
|
79
79
|
codec: str | None = "utf-8"
|
|
80
|
+
# Whether to generate security parameters
|
|
81
|
+
with_security_parameters: bool = True
|
|
80
82
|
# Header generation configuration
|
|
81
83
|
headers: HeaderConfig = field(default_factory=HeaderConfig)
|
schemathesis/models.py
CHANGED
|
@@ -492,6 +492,7 @@ class Case:
|
|
|
492
492
|
query=fast_deepcopy(self.query),
|
|
493
493
|
body=fast_deepcopy(self.body),
|
|
494
494
|
generation_time=self.generation_time,
|
|
495
|
+
id=self.id,
|
|
495
496
|
)
|
|
496
497
|
|
|
497
498
|
|
|
@@ -675,9 +676,11 @@ class APIOperation(Generic[P, C]):
|
|
|
675
676
|
def get_security_requirements(self) -> list[str]:
|
|
676
677
|
return self.schema.get_security_requirements(self)
|
|
677
678
|
|
|
678
|
-
def get_strategies_from_examples(
|
|
679
|
+
def get_strategies_from_examples(
|
|
680
|
+
self, as_strategy_kwargs: dict[str, Any] | None = None
|
|
681
|
+
) -> list[st.SearchStrategy[Case]]:
|
|
679
682
|
"""Get examples from the API operation."""
|
|
680
|
-
return self.schema.get_strategies_from_examples(self)
|
|
683
|
+
return self.schema.get_strategies_from_examples(self, as_strategy_kwargs=as_strategy_kwargs)
|
|
681
684
|
|
|
682
685
|
def get_stateful_tests(self, response: GenericResponse, stateful: Stateful | None) -> Sequence[StatefulTest]:
|
|
683
686
|
return self.schema.get_stateful_tests(response, self, stateful)
|
|
@@ -829,6 +832,7 @@ class Request:
|
|
|
829
832
|
method: str
|
|
830
833
|
uri: str
|
|
831
834
|
body: str | None
|
|
835
|
+
body_size: int | None
|
|
832
836
|
headers: Headers
|
|
833
837
|
|
|
834
838
|
@classmethod
|
|
@@ -859,6 +863,7 @@ class Request:
|
|
|
859
863
|
method=method,
|
|
860
864
|
headers={key: [value] for (key, value) in prepared.headers.items()},
|
|
861
865
|
body=serialize_payload(body) if body is not None else body,
|
|
866
|
+
body_size=len(body) if body is not None else None,
|
|
862
867
|
)
|
|
863
868
|
|
|
864
869
|
def deserialize_body(self) -> bytes | None:
|
|
@@ -878,6 +883,7 @@ class Response:
|
|
|
878
883
|
message: str
|
|
879
884
|
headers: dict[str, list[str]]
|
|
880
885
|
body: str | None
|
|
886
|
+
body_size: int | None
|
|
881
887
|
encoding: str | None
|
|
882
888
|
http_version: str
|
|
883
889
|
elapsed: float
|
|
@@ -904,6 +910,7 @@ class Response:
|
|
|
904
910
|
status_code=response.status_code,
|
|
905
911
|
message=response.reason,
|
|
906
912
|
body=body,
|
|
913
|
+
body_size=len(response.content) if body is not None else None,
|
|
907
914
|
encoding=response.encoding,
|
|
908
915
|
headers=headers,
|
|
909
916
|
http_version=http_version,
|
|
@@ -931,6 +938,7 @@ class Response:
|
|
|
931
938
|
status_code=response.status_code,
|
|
932
939
|
message=message,
|
|
933
940
|
body=body,
|
|
941
|
+
body_size=len(data) if body is not None else None,
|
|
934
942
|
encoding=encoding,
|
|
935
943
|
headers=headers,
|
|
936
944
|
http_version="1.1",
|
|
@@ -947,6 +955,9 @@ class Response:
|
|
|
947
955
|
return deserialize_payload(self.body)
|
|
948
956
|
|
|
949
957
|
|
|
958
|
+
TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
|
|
959
|
+
|
|
960
|
+
|
|
950
961
|
@dataclass
|
|
951
962
|
class Interaction:
|
|
952
963
|
"""A single interaction with the target app."""
|
|
@@ -956,7 +967,7 @@ class Interaction:
|
|
|
956
967
|
checks: list[Check]
|
|
957
968
|
status: Status
|
|
958
969
|
data_generation_method: DataGenerationMethod
|
|
959
|
-
recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
|
|
970
|
+
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
960
971
|
|
|
961
972
|
@classmethod
|
|
962
973
|
def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
|
schemathesis/runner/events.py
CHANGED
schemathesis/runner/impl/core.py
CHANGED
|
@@ -236,12 +236,54 @@ class BaseRunner:
|
|
|
236
236
|
state_machine = self.schema.as_state_machine()
|
|
237
237
|
runner = state_machine.runner(config=config)
|
|
238
238
|
status = Status.success
|
|
239
|
+
|
|
240
|
+
def from_step_status(step_status: stateful_events.StepStatus) -> Status:
|
|
241
|
+
return {
|
|
242
|
+
stateful_events.StepStatus.SUCCESS: Status.success,
|
|
243
|
+
stateful_events.StepStatus.FAILURE: Status.failure,
|
|
244
|
+
stateful_events.StepStatus.ERROR: Status.error,
|
|
245
|
+
stateful_events.StepStatus.INTERRUPTED: Status.error,
|
|
246
|
+
}[step_status]
|
|
247
|
+
|
|
248
|
+
if self.store_interactions:
|
|
249
|
+
if isinstance(state_machine.schema.transport, RequestsTransport):
|
|
250
|
+
|
|
251
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
252
|
+
if event.response is not None:
|
|
253
|
+
response = cast(requests.Response, event.response)
|
|
254
|
+
result.store_requests_response(
|
|
255
|
+
status=from_step_status(event.status),
|
|
256
|
+
case=event.case,
|
|
257
|
+
response=response,
|
|
258
|
+
checks=event.checks,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
|
|
263
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
264
|
+
if event.response is not None:
|
|
265
|
+
response = cast(WSGIResponse, event.response)
|
|
266
|
+
result.store_wsgi_response(
|
|
267
|
+
status=from_step_status(event.status),
|
|
268
|
+
case=event.case,
|
|
269
|
+
response=response,
|
|
270
|
+
headers=self.headers or {},
|
|
271
|
+
elapsed=response.elapsed.total_seconds(),
|
|
272
|
+
checks=event.checks,
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
|
|
276
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
277
|
+
return None
|
|
278
|
+
|
|
239
279
|
for stateful_event in runner.execute():
|
|
240
280
|
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
241
281
|
if stateful_event.failures and status != Status.error:
|
|
242
282
|
status = Status.failure
|
|
243
283
|
for failure in stateful_event.failures:
|
|
244
284
|
result.checks.append(failure)
|
|
285
|
+
elif isinstance(stateful_event, stateful_events.StepFinished):
|
|
286
|
+
on_step_finished(stateful_event)
|
|
245
287
|
elif isinstance(stateful_event, stateful_events.Errored):
|
|
246
288
|
status = Status.error
|
|
247
289
|
yield events.StatefulEvent(data=stateful_event)
|
|
@@ -249,6 +291,7 @@ class BaseRunner:
|
|
|
249
291
|
yield events.AfterStatefulExecution(
|
|
250
292
|
status=status,
|
|
251
293
|
result=SerializedTestResult.from_test_result(result),
|
|
294
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
252
295
|
)
|
|
253
296
|
|
|
254
297
|
def _run_tests(
|
schemathesis/schemas.py
CHANGED
|
@@ -173,7 +173,9 @@ class BaseSchema(Mapping):
|
|
|
173
173
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
174
174
|
raise NotImplementedError
|
|
175
175
|
|
|
176
|
-
def get_strategies_from_examples(
|
|
176
|
+
def get_strategies_from_examples(
|
|
177
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
178
|
+
) -> list[SearchStrategy[Case]]:
|
|
177
179
|
"""Get examples from the API operation."""
|
|
178
180
|
raise NotImplementedError
|
|
179
181
|
|
|
@@ -171,13 +171,36 @@ def _serialize_stateful_event(event: stateful_events.StatefulEvent) -> dict[str,
|
|
|
171
171
|
"timestamp": event.timestamp,
|
|
172
172
|
"exception": format_exception(event.exception, True),
|
|
173
173
|
}
|
|
174
|
+
elif isinstance(event, stateful_events.StepFinished):
|
|
175
|
+
data = {
|
|
176
|
+
"timestamp": event.timestamp,
|
|
177
|
+
"status": event.status,
|
|
178
|
+
"transition_id": {
|
|
179
|
+
"name": event.transition_id.name,
|
|
180
|
+
"status_code": event.transition_id.status_code,
|
|
181
|
+
"source": event.transition_id.source,
|
|
182
|
+
}
|
|
183
|
+
if event.transition_id is not None
|
|
184
|
+
else None,
|
|
185
|
+
"target": event.target,
|
|
186
|
+
"response": {
|
|
187
|
+
"status_code": event.response.status_code,
|
|
188
|
+
"elapsed": event.response.elapsed.total_seconds(),
|
|
189
|
+
}
|
|
190
|
+
if event.response is not None
|
|
191
|
+
else None,
|
|
192
|
+
}
|
|
174
193
|
else:
|
|
175
194
|
data = asdict(event)
|
|
176
195
|
return {"data": {event.__class__.__name__: data}}
|
|
177
196
|
|
|
178
197
|
|
|
179
198
|
def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
|
|
180
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
"status": event.status,
|
|
201
|
+
"data_generation_method": event.data_generation_method,
|
|
202
|
+
"result": asdict(event.result),
|
|
203
|
+
}
|
|
181
204
|
|
|
182
205
|
|
|
183
206
|
SERIALIZER_MAP = {
|
|
@@ -249,7 +249,9 @@ class GraphQLSchema(BaseSchema):
|
|
|
249
249
|
**kwargs,
|
|
250
250
|
)
|
|
251
251
|
|
|
252
|
-
def get_strategies_from_examples(
|
|
252
|
+
def get_strategies_from_examples(
|
|
253
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
254
|
+
) -> list[SearchStrategy[Case]]:
|
|
253
255
|
return []
|
|
254
256
|
|
|
255
257
|
def get_stateful_tests(
|
|
@@ -42,7 +42,9 @@ class BodyExample:
|
|
|
42
42
|
Example = Union[ParameterExample, BodyExample]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def get_strategies_from_examples(
|
|
45
|
+
def get_strategies_from_examples(
|
|
46
|
+
operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
|
|
47
|
+
) -> list[SearchStrategy[Case]]:
|
|
46
48
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
|
47
49
|
maps = {}
|
|
48
50
|
for location, container in LOCATION_TO_CONTAINER.items():
|
|
@@ -65,7 +67,7 @@ def get_strategies_from_examples(operation: APIOperation[OpenAPIParameter, Case]
|
|
|
65
67
|
# Add examples from parameter's schemas
|
|
66
68
|
examples.extend(extract_from_schemas(operation))
|
|
67
69
|
return [
|
|
68
|
-
get_case_strategy(operation=operation, **parameters).map(serialize_components)
|
|
70
|
+
get_case_strategy(operation=operation, **{**parameters, **(as_strategy_kwargs or {})}).map(serialize_components)
|
|
69
71
|
for parameters in produce_combinations(examples)
|
|
70
72
|
]
|
|
71
73
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from functools import lru_cache
|
|
5
6
|
from typing import Any, Callable, Dict, Union, overload
|
|
@@ -7,6 +8,7 @@ from urllib.request import urlopen
|
|
|
7
8
|
|
|
8
9
|
import jsonschema
|
|
9
10
|
import requests
|
|
11
|
+
from jsonschema.exceptions import RefResolutionError
|
|
10
12
|
|
|
11
13
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
12
14
|
from ...internal.copy import fast_deepcopy
|
|
@@ -55,6 +57,23 @@ class InliningResolver(jsonschema.RefResolver):
|
|
|
55
57
|
)
|
|
56
58
|
super().__init__(*args, **kwargs)
|
|
57
59
|
|
|
60
|
+
if sys.version_info >= (3, 11):
|
|
61
|
+
|
|
62
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
63
|
+
try:
|
|
64
|
+
return super().resolve(ref)
|
|
65
|
+
except RefResolutionError as exc:
|
|
66
|
+
exc.add_note(ref)
|
|
67
|
+
raise
|
|
68
|
+
else:
|
|
69
|
+
|
|
70
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
71
|
+
try:
|
|
72
|
+
return super().resolve(ref)
|
|
73
|
+
except RefResolutionError as exc:
|
|
74
|
+
exc.__notes__ = [ref]
|
|
75
|
+
raise
|
|
76
|
+
|
|
58
77
|
@overload
|
|
59
78
|
def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
|
|
60
79
|
pass
|
|
@@ -77,7 +77,13 @@ from .parameters import (
|
|
|
77
77
|
OpenAPI30Parameter,
|
|
78
78
|
OpenAPIParameter,
|
|
79
79
|
)
|
|
80
|
-
from .references import
|
|
80
|
+
from .references import (
|
|
81
|
+
RECURSION_DEPTH_LIMIT,
|
|
82
|
+
UNRESOLVABLE,
|
|
83
|
+
ConvertingResolver,
|
|
84
|
+
InliningResolver,
|
|
85
|
+
resolve_pointer,
|
|
86
|
+
)
|
|
81
87
|
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
|
82
88
|
from .stateful import create_state_machine
|
|
83
89
|
|
|
@@ -385,7 +391,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
385
391
|
)
|
|
386
392
|
for parameter in parameters:
|
|
387
393
|
operation.add_parameter(parameter)
|
|
388
|
-
self.
|
|
394
|
+
if self.generation_config.with_security_parameters:
|
|
395
|
+
self.security.process_definitions(self.raw_schema, operation, self.resolver)
|
|
389
396
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
|
390
397
|
return operation
|
|
391
398
|
|
|
@@ -399,7 +406,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
399
406
|
"""Content types available for this API operation."""
|
|
400
407
|
raise NotImplementedError
|
|
401
408
|
|
|
402
|
-
def get_strategies_from_examples(
|
|
409
|
+
def get_strategies_from_examples(
|
|
410
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
411
|
+
) -> list[SearchStrategy[Case]]:
|
|
403
412
|
"""Get examples from the API operation."""
|
|
404
413
|
raise NotImplementedError
|
|
405
414
|
|
|
@@ -505,12 +514,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
505
514
|
|
|
506
515
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
|
507
516
|
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
517
|
+
if self.generation_config.with_security_parameters:
|
|
518
|
+
security_parameters = self.security.get_security_definitions_as_parameters(
|
|
519
|
+
self.raw_schema, operation, self.resolver, location
|
|
520
|
+
)
|
|
521
|
+
security_parameters = [item for item in security_parameters if item["in"] == location]
|
|
522
|
+
if security_parameters:
|
|
523
|
+
definitions.extend(security_parameters)
|
|
514
524
|
if definitions:
|
|
515
525
|
return self._get_parameter_serializer(definitions)
|
|
516
526
|
return None
|
|
@@ -924,9 +934,11 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
924
934
|
)
|
|
925
935
|
return collected
|
|
926
936
|
|
|
927
|
-
def get_strategies_from_examples(
|
|
937
|
+
def get_strategies_from_examples(
|
|
938
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
939
|
+
) -> list[SearchStrategy[Case]]:
|
|
928
940
|
"""Get examples from the API operation."""
|
|
929
|
-
return get_strategies_from_examples(operation)
|
|
941
|
+
return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
|
|
930
942
|
|
|
931
943
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
932
944
|
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
|
@@ -1088,9 +1100,11 @@ class OpenApi30(SwaggerV20):
|
|
|
1088
1100
|
return scopes, to_json_schema_recursive(option["schema"], self.nullable_name, is_response_schema=True)
|
|
1089
1101
|
return scopes, None
|
|
1090
1102
|
|
|
1091
|
-
def get_strategies_from_examples(
|
|
1103
|
+
def get_strategies_from_examples(
|
|
1104
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
1105
|
+
) -> list[SearchStrategy[Case]]:
|
|
1092
1106
|
"""Get examples from the API operation."""
|
|
1093
|
-
return get_strategies_from_examples(operation)
|
|
1107
|
+
return get_strategies_from_examples(operation, as_strategy_kwargs=as_strategy_kwargs)
|
|
1094
1108
|
|
|
1095
1109
|
def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
|
|
1096
1110
|
definitions = self._get_response_definitions(operation, response)
|
|
@@ -126,10 +126,23 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
|
126
126
|
"""In Open API 3 security definitions are located in ``components`` and may have references inside."""
|
|
127
127
|
components = schema.get("components", {})
|
|
128
128
|
security_schemes = components.get("securitySchemes", {})
|
|
129
|
+
# At this point, the resolution scope could differ from the root scope, that's why we need to restore it
|
|
130
|
+
# as now we resolve root-level references
|
|
131
|
+
if len(resolver._scopes_stack) > 1:
|
|
132
|
+
scope = resolver.resolution_scope
|
|
133
|
+
resolver.pop_scope()
|
|
134
|
+
else:
|
|
135
|
+
scope = None
|
|
129
136
|
resolve = resolver.resolve
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
try:
|
|
138
|
+
if "$ref" in security_schemes:
|
|
139
|
+
return resolve(security_schemes["$ref"])[1]
|
|
140
|
+
return {
|
|
141
|
+
key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()
|
|
142
|
+
}
|
|
143
|
+
finally:
|
|
144
|
+
if scope is not None:
|
|
145
|
+
resolver._scopes_stack.append(scope)
|
|
133
146
|
|
|
134
147
|
def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
135
148
|
schema = make_auth_header_schema(definition)
|
schemathesis/stateful/context.py
CHANGED
|
@@ -39,6 +39,8 @@ class RunnerContext:
|
|
|
39
39
|
seen_in_suite: set[FailureKey] = field(default_factory=set)
|
|
40
40
|
# Unique failures collected in the current suite
|
|
41
41
|
failures_for_suite: list[Check] = field(default_factory=list)
|
|
42
|
+
# All checks executed in the current run
|
|
43
|
+
checks_for_step: list[Check] = field(default_factory=list)
|
|
42
44
|
# Status of the current step
|
|
43
45
|
current_step_status: events.StepStatus | None = None
|
|
44
46
|
current_response: GenericResponse | None = None
|
|
@@ -55,10 +57,13 @@ class RunnerContext:
|
|
|
55
57
|
return events.ScenarioStatus.INTERRUPTED
|
|
56
58
|
return events.ScenarioStatus.REJECTED
|
|
57
59
|
|
|
58
|
-
def
|
|
60
|
+
def reset_scenario(self) -> None:
|
|
59
61
|
self.current_step_status = None
|
|
60
62
|
self.current_response = None
|
|
61
63
|
|
|
64
|
+
def reset_step(self) -> None:
|
|
65
|
+
self.checks_for_step = []
|
|
66
|
+
|
|
62
67
|
def step_succeeded(self) -> None:
|
|
63
68
|
self.current_step_status = events.StepStatus.SUCCESS
|
|
64
69
|
|
|
@@ -100,4 +105,4 @@ class RunnerContext:
|
|
|
100
105
|
def reset(self) -> None:
|
|
101
106
|
self.failures_for_suite = []
|
|
102
107
|
self.seen_in_suite.clear()
|
|
103
|
-
self.
|
|
108
|
+
self.reset_scenario()
|
schemathesis/stateful/events.py
CHANGED
|
@@ -6,7 +6,8 @@ from enum import Enum
|
|
|
6
6
|
from typing import TYPE_CHECKING, Type
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from ..models import Check
|
|
9
|
+
from ..models import Case, Check
|
|
10
|
+
from ..transports.responses import GenericResponse
|
|
10
11
|
from .state_machine import APIStateMachine
|
|
11
12
|
|
|
12
13
|
|
|
@@ -178,17 +179,28 @@ class StepFinished(StatefulEvent):
|
|
|
178
179
|
status: StepStatus
|
|
179
180
|
transition_id: TransitionId | None
|
|
180
181
|
target: str
|
|
181
|
-
|
|
182
|
+
case: Case
|
|
183
|
+
response: GenericResponse | None
|
|
184
|
+
checks: list[Check]
|
|
182
185
|
|
|
183
|
-
__slots__ = ("timestamp", "status", "transition_id", "target", "response")
|
|
186
|
+
__slots__ = ("timestamp", "status", "transition_id", "target", "case", "response", "checks")
|
|
184
187
|
|
|
185
188
|
def __init__(
|
|
186
|
-
self,
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
status: StepStatus,
|
|
192
|
+
transition_id: TransitionId | None,
|
|
193
|
+
target: str,
|
|
194
|
+
case: Case,
|
|
195
|
+
response: GenericResponse | None,
|
|
196
|
+
checks: list[Check],
|
|
187
197
|
) -> None:
|
|
188
198
|
self.status = status
|
|
189
199
|
self.transition_id = transition_id
|
|
190
200
|
self.target = target
|
|
201
|
+
self.case = case
|
|
191
202
|
self.response = response
|
|
203
|
+
self.checks = checks
|
|
192
204
|
self.timestamp = time.monotonic()
|
|
193
205
|
|
|
194
206
|
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -157,30 +157,25 @@ def _execute_state_machine_loop(
|
|
|
157
157
|
)
|
|
158
158
|
else:
|
|
159
159
|
transition_id = None
|
|
160
|
-
response: events.ResponseData | None
|
|
161
|
-
if ctx.current_response is not None:
|
|
162
|
-
response = events.ResponseData(
|
|
163
|
-
status_code=ctx.current_response.status_code,
|
|
164
|
-
elapsed=ctx.current_response.elapsed.total_seconds(),
|
|
165
|
-
)
|
|
166
|
-
else:
|
|
167
|
-
response = None
|
|
168
160
|
status = cast(events.StepStatus, ctx.current_step_status)
|
|
169
161
|
event_queue.put(
|
|
170
162
|
events.StepFinished(
|
|
171
163
|
status=status,
|
|
172
164
|
transition_id=transition_id,
|
|
173
165
|
target=case.operation.verbose_name,
|
|
174
|
-
|
|
166
|
+
case=case,
|
|
167
|
+
response=ctx.current_response,
|
|
168
|
+
checks=ctx.checks_for_step,
|
|
175
169
|
)
|
|
176
170
|
)
|
|
171
|
+
ctx.reset_step()
|
|
177
172
|
return result
|
|
178
173
|
|
|
179
174
|
def validate_response(
|
|
180
175
|
self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
|
181
176
|
) -> None:
|
|
182
177
|
ctx.current_response = response
|
|
183
|
-
validate_response(response, case, ctx, config.checks, additional_checks)
|
|
178
|
+
validate_response(response, case, ctx, config.checks, ctx.checks_for_step, additional_checks)
|
|
184
179
|
|
|
185
180
|
def teardown(self) -> None:
|
|
186
181
|
build_ctx = current_build_context()
|
|
@@ -190,7 +185,7 @@ def _execute_state_machine_loop(
|
|
|
190
185
|
is_final=build_ctx.is_final,
|
|
191
186
|
)
|
|
192
187
|
)
|
|
193
|
-
ctx.
|
|
188
|
+
ctx.reset_scenario()
|
|
194
189
|
super().teardown()
|
|
195
190
|
|
|
196
191
|
while True:
|
schemathesis/stateful/sink.py
CHANGED
|
@@ -51,7 +51,7 @@ class StateMachineSink:
|
|
|
51
51
|
responses = self.response_times.setdefault(event.target, {})
|
|
52
52
|
if event.response is not None:
|
|
53
53
|
average = responses.setdefault(event.response.status_code, AverageResponseTime())
|
|
54
|
-
average.total += event.response.elapsed
|
|
54
|
+
average.total += event.response.elapsed.total_seconds()
|
|
55
55
|
average.count += 1
|
|
56
56
|
elif isinstance(event, events.ScenarioFinished):
|
|
57
57
|
self.scenarios[event.status] += 1
|
|
@@ -7,7 +7,7 @@ from .context import RunnerContext
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from ..failures import FailureContext
|
|
10
|
-
from ..models import Case, CheckFunction
|
|
10
|
+
from ..models import Case, CheckFunction, Check
|
|
11
11
|
from ..transports.responses import GenericResponse
|
|
12
12
|
|
|
13
13
|
|
|
@@ -16,6 +16,7 @@ def validate_response(
|
|
|
16
16
|
case: Case,
|
|
17
17
|
failures: RunnerContext,
|
|
18
18
|
checks: tuple[CheckFunction, ...],
|
|
19
|
+
check_results: list[Check],
|
|
19
20
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
20
21
|
) -> None:
|
|
21
22
|
"""Validate the response against the provided checks."""
|
|
@@ -28,18 +29,18 @@ def validate_response(
|
|
|
28
29
|
exceptions.append(exc)
|
|
29
30
|
if failures.is_seen_in_suite(exc):
|
|
30
31
|
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
request=None,
|
|
41
|
-
)
|
|
32
|
+
failed_check = Check(
|
|
33
|
+
name=name,
|
|
34
|
+
value=Status.failure,
|
|
35
|
+
response=response,
|
|
36
|
+
elapsed=response.elapsed.total_seconds(),
|
|
37
|
+
example=copied_case,
|
|
38
|
+
message=message,
|
|
39
|
+
context=context,
|
|
40
|
+
request=None,
|
|
42
41
|
)
|
|
42
|
+
failures.add_failed_check(failed_check)
|
|
43
|
+
check_results.append(failed_check)
|
|
43
44
|
failures.mark_as_seen_in_suite(exc)
|
|
44
45
|
|
|
45
46
|
for check in checks + additional_checks:
|
|
@@ -47,6 +48,17 @@ def validate_response(
|
|
|
47
48
|
copied_case = case.partial_deepcopy()
|
|
48
49
|
try:
|
|
49
50
|
check(response, copied_case)
|
|
51
|
+
skip_check = check(response, copied_case)
|
|
52
|
+
if not skip_check:
|
|
53
|
+
passed_check = Check(
|
|
54
|
+
name=name,
|
|
55
|
+
value=Status.success,
|
|
56
|
+
response=response,
|
|
57
|
+
elapsed=response.elapsed.total_seconds(),
|
|
58
|
+
example=copied_case,
|
|
59
|
+
request=None,
|
|
60
|
+
)
|
|
61
|
+
check_results.append(passed_check)
|
|
50
62
|
except CheckFailed as exc:
|
|
51
63
|
if failures.is_seen_in_run(exc):
|
|
52
64
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.31.0
|
|
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://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -31,6 +31,7 @@ Requires-Python: >=3.8
|
|
|
31
31
|
Requires-Dist: backoff<3.0,>=2.1.2
|
|
32
32
|
Requires-Dist: click<9.0,>=7.0
|
|
33
33
|
Requires-Dist: colorama<1.0,>=0.4
|
|
34
|
+
Requires-Dist: harfile<1.0,>=0.3.0
|
|
34
35
|
Requires-Dist: httpx<1.0,>=0.22.0
|
|
35
36
|
Requires-Dist: hypothesis-graphql<1,>=0.11.0
|
|
36
37
|
Requires-Dist: hypothesis-jsonschema<0.24,>=0.23.1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
schemathesis/__init__.py,sha256=pNaTfaC3NSdediNQuH9QAcuIx3U-MmSydvhS65FZrxw,1984
|
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
|
4
|
+
schemathesis/_hypothesis.py,sha256=Igb1x3am3tafVhZ6ikY7yrmQdNthC0PZFEYVCdNAA3w,11207
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=3CbA7P9Q89W3ymaYxiOV5Xpv1yhoBqroLK4YRpYMjX4,1630
|
|
7
7
|
schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
|
|
@@ -10,27 +10,27 @@ schemathesis/auths.py,sha256=NeJqsjtDgJtHMyrHc6V1NTpkAh1K8ZKLalpB3v80cT4,14734
|
|
|
10
10
|
schemathesis/checks.py,sha256=XplKduiRbRrxZx-wDMGaW91aIitr5RL9vjMP6bvCFRg,2272
|
|
11
11
|
schemathesis/code_samples.py,sha256=xk1-1jnXg5hS40VzIZp8PEtZwGaazNlVKMT7_X-zG-M,4123
|
|
12
12
|
schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
|
|
13
|
-
schemathesis/exceptions.py,sha256=
|
|
13
|
+
schemathesis/exceptions.py,sha256=tFDlNui1Sxc8a6-OQCIjj62SoToYoEN9zveguPpzQIc,19712
|
|
14
14
|
schemathesis/failures.py,sha256=wXz5Kr5i-ojcYc-BdzFlNbNGOfoVXHZM6kd4iULdHK4,7003
|
|
15
15
|
schemathesis/filters.py,sha256=0fYzn9sJ35k3Znx1P8FrbSdoUcdslcibtGh-IOTRwB8,10251
|
|
16
16
|
schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
17
17
|
schemathesis/hooks.py,sha256=dveqMmThIvt4fDahUXhU2nCq5pFvYjzzd1Ys_MhrJZA,12398
|
|
18
18
|
schemathesis/lazy.py,sha256=eVdGkTZK0fWvUlFUCFGGlViH2NWEtYIjxiNkF4fBWhI,15218
|
|
19
19
|
schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=4kAiutx5BEZ4h4AHMvZVW7gJpObur7kkGFuuF2cTEkU,44491
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=mRR4YvXpzqbmgX8Xu6rume6LBcz9g_oyusvbesZl44I,8958
|
|
24
|
-
schemathesis/schemas.py,sha256=
|
|
24
|
+
schemathesis/schemas.py,sha256=nUJIfR0mXYpKwEX3mZiS3hpUDdkD3vulROVeovL0dqU,18331
|
|
25
25
|
schemathesis/serializers.py,sha256=kxXZ-UGa1v_vOm0sC4QYcrNv4rfvI7tHGT2elRVbCbc,11649
|
|
26
26
|
schemathesis/targets.py,sha256=N1Zzgqa1PNycWIeGpra7q-6ASn2x4r9Jompn35bmlsE,1163
|
|
27
27
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
28
28
|
schemathesis/types.py,sha256=xOzNAeMs6qqeaJnWs5Fpw5JPbvVjyfRfxTJa3G2Ln5I,920
|
|
29
29
|
schemathesis/utils.py,sha256=NX04p9mO-lCAH3DIISXDXPxWZk6lkGNM4-ubRi8vlvY,5234
|
|
30
|
-
schemathesis/cli/__init__.py,sha256=
|
|
30
|
+
schemathesis/cli/__init__.py,sha256=Gw2uVput5JySsSg8gjAzibi-tK_PixwyNYyQLTukAJE,66535
|
|
31
31
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
32
|
-
schemathesis/cli/callbacks.py,sha256=
|
|
33
|
-
schemathesis/cli/cassettes.py,sha256=
|
|
32
|
+
schemathesis/cli/callbacks.py,sha256=R2noVRu8zDBWmA3dJ2YnmhAjYdkxPDA1zIpA3_2DkFQ,15144
|
|
33
|
+
schemathesis/cli/cassettes.py,sha256=3vumBEP0XXBVpNvwMEGYvcIp1TSHtCNUIL60TPDVgD4,19024
|
|
34
34
|
schemathesis/cli/constants.py,sha256=ogSuZs68KvzHNKF0yaiBkxLcGbo8IVR3xaIfsy1H1IQ,1546
|
|
35
35
|
schemathesis/cli/context.py,sha256=EMeyAbU9mRujR46anc43yr6ab4rGYtIDaHC3cV9Qa-Q,2092
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
@@ -57,7 +57,7 @@ schemathesis/extra/pytest_plugin.py,sha256=ymicV2NjmSzee0ccUUUjNEvb9ihCxxf_8M60g
|
|
|
57
57
|
schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
|
|
58
58
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
|
59
59
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
|
60
|
-
schemathesis/generation/__init__.py,sha256=
|
|
60
|
+
schemathesis/generation/__init__.py,sha256=mC1NVAHyce1_B_wu-GYO9U21Gut8KFrjPXETREC9ABQ,2366
|
|
61
61
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
62
62
|
schemathesis/internal/copy.py,sha256=lcK01CODz6jogXyH0tsKkPv1PBEX8jeBPxI9MzQ6LN4,942
|
|
63
63
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
@@ -68,11 +68,11 @@ schemathesis/internal/result.py,sha256=d449YvyONjqjDs-A5DAPgtAI96iT753K8sU6_1HLo
|
|
|
68
68
|
schemathesis/internal/transformation.py,sha256=3S6AzAqdsEsB5iobFgSuvL0UMUqH0JHC7hGxKwcpqPw,450
|
|
69
69
|
schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
|
|
70
70
|
schemathesis/runner/__init__.py,sha256=n-4CanmlfMiGnir6xbZH-fM5a0pe2GloBFj2NTtet-M,21419
|
|
71
|
-
schemathesis/runner/events.py,sha256=
|
|
71
|
+
schemathesis/runner/events.py,sha256=VVFy-qAgxoHBNrexBakq_QX_hx52pnWK-67HgSPpYRw,10694
|
|
72
72
|
schemathesis/runner/probes.py,sha256=J-TT0hOKu9j4htWKBcYKmsomcRxmvOl4WpmnKLVXu8M,5546
|
|
73
73
|
schemathesis/runner/serialization.py,sha256=J8fuG8MSJq3rE3IJs73U1YXWFrNa05k7PGd5Bvq1uec,17356
|
|
74
74
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
75
|
-
schemathesis/runner/impl/core.py,sha256=
|
|
75
|
+
schemathesis/runner/impl/core.py,sha256=Ec_HlCRJdQSluN_JYEt0eh99SFXf0RGX6knPpayPUXw,44148
|
|
76
76
|
schemathesis/runner/impl/solo.py,sha256=MatxThgqKsY2tX_hVwjy78oKFeKejb6dFJoX3kGzW4U,3359
|
|
77
77
|
schemathesis/runner/impl/threadpool.py,sha256=fj2QYoWxIJIxpTCcJQyM_VCRO1YDnW9XQJJnNVFVQxY,15253
|
|
78
78
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
|
@@ -86,7 +86,7 @@ schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg
|
|
|
86
86
|
schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
|
|
87
87
|
schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
|
|
88
88
|
schemathesis/service/report.py,sha256=4A8nf6_KOjDW3x1VXF8gSf_WY2xXp1Cbz-Owl_GeR7o,8294
|
|
89
|
-
schemathesis/service/serialization.py,sha256=
|
|
89
|
+
schemathesis/service/serialization.py,sha256=GFNc1-w1ShZopFdR7OFLT9ZKe8Y2S1hYnlXMrdMj3VE,10600
|
|
90
90
|
schemathesis/service/usage.py,sha256=UbXqxeDq5mAjKkfV4hApZsReZmQHXiqoXUYn_Z6YuZk,2438
|
|
91
91
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
92
|
schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
|
|
@@ -94,7 +94,7 @@ schemathesis/specs/graphql/_cache.py,sha256=7ras3q_InDJBPykgHroerl9f2jFamC8xJD35
|
|
|
94
94
|
schemathesis/specs/graphql/loaders.py,sha256=qxNGL67_AfhoRh0hIxlnJVe6do26vqwWS_TrJtB-Lro,12198
|
|
95
95
|
schemathesis/specs/graphql/nodes.py,sha256=bE3G1kNmqJ8OV4igBvIK-UORrkQA6Nofduf87O3TD9I,541
|
|
96
96
|
schemathesis/specs/graphql/scalars.py,sha256=9tvLTiYVe8A_E8ASA0czz3Z0Mp9lyak7R4wHpAE_jKo,1805
|
|
97
|
-
schemathesis/specs/graphql/schemas.py,sha256=
|
|
97
|
+
schemathesis/specs/graphql/schemas.py,sha256=i6fAW9pYcOplQE7BejP6P8GQ9z6Y43Vx4_feVvbd1B4,13555
|
|
98
98
|
schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
|
|
99
99
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
100
100
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
@@ -103,16 +103,16 @@ schemathesis/specs/openapi/checks.py,sha256=1Fu3Kgai9ySCoGtCrx99Q9oVCEWXgkqHd1gT
|
|
|
103
103
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
104
104
|
schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
|
|
105
105
|
schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
|
|
106
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
|
106
|
+
schemathesis/specs/openapi/examples.py,sha256=5bjmW3BnJVTiLlWZbimdfOzQQFR6m1P9G0FErr9g3WI,15128
|
|
107
107
|
schemathesis/specs/openapi/filters.py,sha256=6Q9eNQ6zCR-NQkUxgnkSDWxfk3hsZuxemBv7v1rhwb4,1437
|
|
108
108
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
109
109
|
schemathesis/specs/openapi/links.py,sha256=2ucOLs50OhCqu0PEdbT_BGUM3fKnHBl97YGISLpAxLY,16023
|
|
110
110
|
schemathesis/specs/openapi/loaders.py,sha256=JJdIz1aT03J9WmUWTLOz6Yhuu69IqmhobQ9_vL6XJ6U,24916
|
|
111
111
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
112
112
|
schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
|
|
113
|
-
schemathesis/specs/openapi/references.py,sha256=
|
|
114
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
115
|
-
schemathesis/specs/openapi/security.py,sha256=
|
|
113
|
+
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
114
|
+
schemathesis/specs/openapi/schemas.py,sha256=UMPfQKndW7HRAeLNlu0zmZVmJMaD7CvRJ8p-c8a93uc,52204
|
|
115
|
+
schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
|
|
116
116
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
117
117
|
schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
|
|
118
118
|
schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
|
|
@@ -132,20 +132,20 @@ schemathesis/specs/openapi/stateful/statistic.py,sha256=EJK4NqeAYRYl1FtU9YEuTLyh
|
|
|
132
132
|
schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmthoT3UcHfDHchOYo,419
|
|
133
133
|
schemathesis/stateful/__init__.py,sha256=qyQJ-9Ect-AWZiAsK63F3BTGu-jZnPCOp1q46YAonkQ,4911
|
|
134
134
|
schemathesis/stateful/config.py,sha256=kuKGLNNk0YrR6G0IzcZh6v0oysHHTPfHi9nHUsfQsF0,2298
|
|
135
|
-
schemathesis/stateful/context.py,sha256=
|
|
136
|
-
schemathesis/stateful/events.py,sha256=
|
|
137
|
-
schemathesis/stateful/runner.py,sha256=
|
|
138
|
-
schemathesis/stateful/sink.py,sha256=
|
|
135
|
+
schemathesis/stateful/context.py,sha256=nYurdKz3yLA4xvOAUBc5hiJikDaMTiIn69RQhskreVU,3932
|
|
136
|
+
schemathesis/stateful/events.py,sha256=VF0jRi4eq5ybQnyIjSvScRMjQK-NwKd-pNZ7ZVJwygk,5215
|
|
137
|
+
schemathesis/stateful/runner.py,sha256=4VDjo_PgSoNxfMVsAOfXlJVXZPhqh8fc76Lt7cXPXZ4,9404
|
|
138
|
+
schemathesis/stateful/sink.py,sha256=yWY9xJeUyOWOsu1tNxCgsDFQVJxK2xgQavJ9vQoxK1I,2441
|
|
139
139
|
schemathesis/stateful/state_machine.py,sha256=H-AzMPTKuCKnoCv0b7XPFDsHkzRftNfbvh5xb2H5Hfk,12156
|
|
140
140
|
schemathesis/stateful/statistic.py,sha256=xPLiCw61ofNXQicqcK_sZyLHiqiGcgQARpwd8AiRubM,487
|
|
141
|
-
schemathesis/stateful/validation.py,sha256=
|
|
141
|
+
schemathesis/stateful/validation.py,sha256=RiXnZBTu6zMnjCpZBLyMwPDJrPTnSHWSijrg34VtRoM,2825
|
|
142
142
|
schemathesis/transports/__init__.py,sha256=fwICPJBLHA7_L4IDAiW1SmxNHXlLD_Eqp-74w9TRRIs,12160
|
|
143
143
|
schemathesis/transports/auth.py,sha256=4z7c-K7lfyyVqgR6X1v4yiE8ewR_ViAznWFTAsCL0RI,405
|
|
144
144
|
schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
|
|
145
145
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
146
146
|
schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
|
|
147
|
-
schemathesis-3.
|
|
148
|
-
schemathesis-3.
|
|
149
|
-
schemathesis-3.
|
|
150
|
-
schemathesis-3.
|
|
151
|
-
schemathesis-3.
|
|
147
|
+
schemathesis-3.31.0.dist-info/METADATA,sha256=xq28YKL2SXo0XpmPf68tqEGoY8khEIjTEnoEPkV4tPo,17706
|
|
148
|
+
schemathesis-3.31.0.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
149
|
+
schemathesis-3.31.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
150
|
+
schemathesis-3.31.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
151
|
+
schemathesis-3.31.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|