schemathesis 3.19.0__py3-none-any.whl → 3.19.1__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/auths.py +20 -20
- schemathesis/cli/__init__.py +20 -20
- schemathesis/cli/cassettes.py +18 -18
- schemathesis/cli/context.py +25 -25
- schemathesis/cli/debug.py +3 -3
- schemathesis/cli/junitxml.py +4 -4
- schemathesis/constants.py +3 -3
- schemathesis/exceptions.py +9 -9
- schemathesis/extra/pytest_plugin.py +1 -1
- schemathesis/failures.py +65 -66
- schemathesis/filters.py +13 -13
- schemathesis/hooks.py +11 -11
- schemathesis/lazy.py +16 -16
- schemathesis/models.py +97 -97
- schemathesis/parameters.py +5 -6
- schemathesis/runner/events.py +55 -55
- schemathesis/runner/impl/core.py +26 -26
- schemathesis/runner/impl/solo.py +6 -7
- schemathesis/runner/impl/threadpool.py +5 -5
- schemathesis/runner/serialization.py +50 -50
- schemathesis/schemas.py +23 -23
- schemathesis/serializers.py +3 -3
- schemathesis/service/ci.py +25 -25
- schemathesis/service/client.py +2 -2
- schemathesis/service/events.py +12 -13
- schemathesis/service/hosts.py +4 -4
- schemathesis/service/metadata.py +14 -15
- schemathesis/service/models.py +12 -13
- schemathesis/service/report.py +30 -31
- schemathesis/service/serialization.py +2 -4
- schemathesis/specs/graphql/schemas.py +8 -8
- schemathesis/specs/openapi/expressions/context.py +4 -4
- schemathesis/specs/openapi/expressions/lexer.py +11 -12
- schemathesis/specs/openapi/expressions/nodes.py +16 -16
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/links.py +15 -17
- schemathesis/specs/openapi/negative/__init__.py +5 -5
- schemathesis/specs/openapi/negative/mutations.py +6 -6
- schemathesis/specs/openapi/parameters.py +12 -13
- schemathesis/specs/openapi/references.py +2 -2
- schemathesis/specs/openapi/schemas.py +11 -15
- schemathesis/specs/openapi/security.py +7 -7
- schemathesis/specs/openapi/stateful/links.py +4 -4
- schemathesis/stateful.py +19 -19
- schemathesis/targets.py +5 -6
- schemathesis/types.py +11 -13
- schemathesis/utils.py +2 -2
- {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/METADATA +2 -3
- {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/RECORD +52 -52
- {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.19.0.dist-info → schemathesis-3.19.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/filters.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
|
2
2
|
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
3
4
|
from functools import partial
|
|
4
5
|
from types import SimpleNamespace
|
|
5
6
|
from typing import TYPE_CHECKING, Callable, List, Optional, Set, Tuple, Union
|
|
6
7
|
|
|
7
|
-
import attr
|
|
8
8
|
from typing_extensions import Protocol
|
|
9
9
|
|
|
10
10
|
from .exceptions import UsageError
|
|
@@ -25,15 +25,15 @@ ERROR_EMPTY_FILTER = "Filter can not be empty"
|
|
|
25
25
|
ERROR_FILTER_EXISTS = "Filter already exists"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@
|
|
28
|
+
@dataclass(repr=False, frozen=True)
|
|
29
29
|
class Matcher:
|
|
30
30
|
"""Encapsulates matching logic by various criteria."""
|
|
31
31
|
|
|
32
|
-
func: Callable[..., bool] =
|
|
32
|
+
func: Callable[..., bool] = field(hash=False, compare=False)
|
|
33
33
|
# A short description of a matcher. Primarily exists for debugging purposes
|
|
34
|
-
label: str =
|
|
34
|
+
label: str = field(hash=False, compare=False)
|
|
35
35
|
# Compare & hash matchers by a pre-computed hash value
|
|
36
|
-
_hash: int
|
|
36
|
+
_hash: int
|
|
37
37
|
|
|
38
38
|
def __repr__(self) -> str:
|
|
39
39
|
return f"<{self.__class__.__name__}: {self.label}>"
|
|
@@ -41,7 +41,7 @@ class Matcher:
|
|
|
41
41
|
@classmethod
|
|
42
42
|
def for_function(cls, func: MatcherFunc) -> "Matcher":
|
|
43
43
|
"""Matcher that uses the given function for matching operations."""
|
|
44
|
-
return cls(func, label=func.__name__,
|
|
44
|
+
return cls(func, label=func.__name__, _hash=hash(func))
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
47
|
def for_value(cls, attribute: str, expected: FilterValue) -> "Matcher":
|
|
@@ -51,7 +51,7 @@ class Matcher:
|
|
|
51
51
|
else:
|
|
52
52
|
func = partial(by_value, attribute=attribute, expected=expected)
|
|
53
53
|
label = f"{attribute}={repr(expected)}"
|
|
54
|
-
return cls(func, label=label,
|
|
54
|
+
return cls(func, label=label, _hash=hash(label))
|
|
55
55
|
|
|
56
56
|
@classmethod
|
|
57
57
|
def for_regex(cls, attribute: str, regex: RegexValue) -> "Matcher":
|
|
@@ -60,7 +60,7 @@ class Matcher:
|
|
|
60
60
|
regex = re.compile(regex)
|
|
61
61
|
func = partial(by_regex, attribute=attribute, regex=regex)
|
|
62
62
|
label = f"{attribute}_regex={repr(regex)}"
|
|
63
|
-
return cls(func, label=label,
|
|
63
|
+
return cls(func, label=label, _hash=hash(label))
|
|
64
64
|
|
|
65
65
|
def match(self, ctx: HasAPIOperation) -> bool:
|
|
66
66
|
"""Whether matcher matches the given operation."""
|
|
@@ -88,11 +88,11 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
|
|
|
88
88
|
return bool(regex.match(value))
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
@
|
|
91
|
+
@dataclass(repr=False, frozen=True)
|
|
92
92
|
class Filter:
|
|
93
93
|
"""Match API operations against a list of matchers."""
|
|
94
94
|
|
|
95
|
-
matchers: Tuple[Matcher, ...]
|
|
95
|
+
matchers: Tuple[Matcher, ...]
|
|
96
96
|
|
|
97
97
|
def __repr__(self) -> str:
|
|
98
98
|
inner = " && ".join(matcher.label for matcher in self.matchers)
|
|
@@ -106,12 +106,12 @@ class Filter:
|
|
|
106
106
|
return all(matcher.match(ctx) for matcher in self.matchers)
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
@
|
|
109
|
+
@dataclass
|
|
110
110
|
class FilterSet:
|
|
111
111
|
"""Combines multiple filters to apply inclusion and exclusion rules on API operations."""
|
|
112
112
|
|
|
113
|
-
_includes: Set[Filter] =
|
|
114
|
-
_excludes: Set[Filter] =
|
|
113
|
+
_includes: Set[Filter] = field(default_factory=set)
|
|
114
|
+
_excludes: Set[Filter] = field(default_factory=set)
|
|
115
115
|
|
|
116
116
|
def apply_to(self, operations: List["APIOperation"]) -> List["APIOperation"]:
|
|
117
117
|
"""Get a filtered list of the given operations that match the filters."""
|
schemathesis/hooks.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
from enum import Enum, unique
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Union, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, Dict, List, Optional, Union, cast
|
|
6
7
|
|
|
7
|
-
import attr
|
|
8
8
|
from hypothesis import strategies as st
|
|
9
9
|
|
|
10
10
|
from .types import GenericTest
|
|
@@ -22,13 +22,13 @@ class HookScope(Enum):
|
|
|
22
22
|
TEST = 3
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
@
|
|
25
|
+
@dataclass
|
|
26
26
|
class RegisteredHook:
|
|
27
|
-
signature: inspect.Signature
|
|
28
|
-
scopes: List[HookScope]
|
|
27
|
+
signature: inspect.Signature
|
|
28
|
+
scopes: List[HookScope]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
@
|
|
31
|
+
@dataclass
|
|
32
32
|
class HookContext:
|
|
33
33
|
"""A context that is passed to some hook functions.
|
|
34
34
|
|
|
@@ -36,23 +36,23 @@ class HookContext:
|
|
|
36
36
|
Might be absent in some cases.
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
operation: Optional["APIOperation"] =
|
|
39
|
+
operation: Optional["APIOperation"] = None
|
|
40
40
|
|
|
41
41
|
@deprecated_property(removed_in="4.0", replacement="operation")
|
|
42
42
|
def endpoint(self) -> Optional["APIOperation"]:
|
|
43
43
|
return self.operation
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@
|
|
46
|
+
@dataclass
|
|
47
47
|
class HookDispatcher:
|
|
48
48
|
"""Generic hook dispatcher.
|
|
49
49
|
|
|
50
50
|
Provides a mechanism to extend Schemathesis in registered hook points.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
scope: HookScope
|
|
54
|
-
_hooks: DefaultDict[str, List[Callable]] =
|
|
55
|
-
_specs: Dict[str, RegisteredHook] = {}
|
|
53
|
+
scope: HookScope
|
|
54
|
+
_hooks: DefaultDict[str, List[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
55
|
+
_specs: ClassVar[Dict[str, RegisteredHook]] = {}
|
|
56
56
|
|
|
57
57
|
def register(self, hook: Union[str, Callable]) -> Callable:
|
|
58
58
|
"""Register a new hook.
|
schemathesis/lazy.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
1
2
|
from inspect import signature
|
|
2
3
|
from typing import Any, Callable, Dict, Generator, Optional, Type, Union
|
|
3
4
|
|
|
4
|
-
import attr
|
|
5
5
|
import pytest
|
|
6
6
|
from _pytest.fixtures import FixtureRequest
|
|
7
7
|
from hypothesis.core import HypothesisHandle
|
|
@@ -33,22 +33,22 @@ from .utils import (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
@
|
|
36
|
+
@dataclass
|
|
37
37
|
class LazySchema:
|
|
38
|
-
fixture_name: str
|
|
39
|
-
base_url: Union[Optional[str], NotSet] =
|
|
40
|
-
method: Optional[Filter] =
|
|
41
|
-
endpoint: Optional[Filter] =
|
|
42
|
-
tag: Optional[Filter] =
|
|
43
|
-
operation_id: Optional[Filter] =
|
|
44
|
-
app: Any =
|
|
45
|
-
hooks: HookDispatcher =
|
|
46
|
-
auth: AuthStorage =
|
|
47
|
-
validate_schema: bool =
|
|
48
|
-
skip_deprecated_operations: bool =
|
|
49
|
-
data_generation_methods: Union[DataGenerationMethodInput, NotSet] =
|
|
50
|
-
code_sample_style: CodeSampleStyle =
|
|
51
|
-
rate_limiter: Optional[Limiter] =
|
|
38
|
+
fixture_name: str
|
|
39
|
+
base_url: Union[Optional[str], NotSet] = NOT_SET
|
|
40
|
+
method: Optional[Filter] = NOT_SET
|
|
41
|
+
endpoint: Optional[Filter] = NOT_SET
|
|
42
|
+
tag: Optional[Filter] = NOT_SET
|
|
43
|
+
operation_id: Optional[Filter] = NOT_SET
|
|
44
|
+
app: Any = NOT_SET
|
|
45
|
+
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
|
46
|
+
auth: AuthStorage = field(default_factory=AuthStorage)
|
|
47
|
+
validate_schema: bool = True
|
|
48
|
+
skip_deprecated_operations: bool = False
|
|
49
|
+
data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET
|
|
50
|
+
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
51
|
+
rate_limiter: Optional[Limiter] = None
|
|
52
52
|
|
|
53
53
|
def hook(self, hook: Union[str, Callable]) -> Callable:
|
|
54
54
|
return self.hooks.register(hook)
|
schemathesis/models.py
CHANGED
|
@@ -4,6 +4,7 @@ import http
|
|
|
4
4
|
import json
|
|
5
5
|
from collections import Counter
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass, field
|
|
7
8
|
from enum import Enum
|
|
8
9
|
from itertools import chain
|
|
9
10
|
from logging import LogRecord
|
|
@@ -28,7 +29,6 @@ from typing import (
|
|
|
28
29
|
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
29
30
|
from uuid import uuid4
|
|
30
31
|
|
|
31
|
-
import attr
|
|
32
32
|
import curlify
|
|
33
33
|
import requests.auth
|
|
34
34
|
import werkzeug
|
|
@@ -77,13 +77,13 @@ if TYPE_CHECKING:
|
|
|
77
77
|
from .stateful import Stateful, StatefulTest
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
@
|
|
80
|
+
@dataclass
|
|
81
81
|
class CaseSource:
|
|
82
82
|
"""Data sources, used to generate a test case."""
|
|
83
83
|
|
|
84
|
-
case: "Case"
|
|
85
|
-
response: GenericResponse
|
|
86
|
-
elapsed: float
|
|
84
|
+
case: "Case"
|
|
85
|
+
response: GenericResponse
|
|
86
|
+
elapsed: float
|
|
87
87
|
|
|
88
88
|
def partial_deepcopy(self) -> "CaseSource":
|
|
89
89
|
return self.__class__(
|
|
@@ -110,27 +110,27 @@ def serialize(value: Any) -> str:
|
|
|
110
110
|
return json.dumps(value, sort_keys=True, default=_serialize_unknown)
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
@
|
|
113
|
+
@dataclass(repr=False)
|
|
114
114
|
class Case:
|
|
115
115
|
"""A single test case parameters."""
|
|
116
116
|
|
|
117
|
-
operation: "APIOperation"
|
|
118
|
-
path_parameters: Optional[PathParameters] =
|
|
119
|
-
headers: Optional[CaseInsensitiveDict] =
|
|
120
|
-
cookies: Optional[Cookies] =
|
|
121
|
-
query: Optional[Query] =
|
|
117
|
+
operation: "APIOperation"
|
|
118
|
+
path_parameters: Optional[PathParameters] = None
|
|
119
|
+
headers: Optional[CaseInsensitiveDict] = None
|
|
120
|
+
cookies: Optional[Cookies] = None
|
|
121
|
+
query: Optional[Query] = None
|
|
122
122
|
# By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
|
|
123
123
|
# which is a valid payload.
|
|
124
|
-
body: Union[Body, NotSet] =
|
|
124
|
+
body: Union[Body, NotSet] = NOT_SET
|
|
125
125
|
|
|
126
|
-
source: Optional[CaseSource] =
|
|
126
|
+
source: Optional[CaseSource] = None
|
|
127
127
|
# The media type for cases with a payload. For example, "application/json"
|
|
128
|
-
media_type: Optional[str] =
|
|
128
|
+
media_type: Optional[str] = None
|
|
129
129
|
# The way the case was generated (None for manually crafted ones)
|
|
130
|
-
data_generation_method: Optional[DataGenerationMethod] =
|
|
130
|
+
data_generation_method: Optional[DataGenerationMethod] = None
|
|
131
131
|
# Unique test case identifier
|
|
132
|
-
id: str =
|
|
133
|
-
_auth: Optional[requests.auth.AuthBase] =
|
|
132
|
+
id: str = field(default_factory=lambda: uuid4().hex, compare=False)
|
|
133
|
+
_auth: Optional[requests.auth.AuthBase] = None
|
|
134
134
|
|
|
135
135
|
def __repr__(self) -> str:
|
|
136
136
|
parts = [f"{self.__class__.__name__}("]
|
|
@@ -308,7 +308,7 @@ class Case:
|
|
|
308
308
|
if "content-type" not in {header.lower() for header in final_headers}:
|
|
309
309
|
final_headers["Content-Type"] = self.media_type
|
|
310
310
|
base_url = self._get_base_url(base_url)
|
|
311
|
-
formatted_path = self.formatted_path.lstrip("/")
|
|
311
|
+
formatted_path = self.formatted_path.lstrip("/")
|
|
312
312
|
if not base_url.endswith("/"):
|
|
313
313
|
base_url += "/"
|
|
314
314
|
url = unquote(urljoin(base_url, quote(formatted_path)))
|
|
@@ -537,11 +537,11 @@ class Case:
|
|
|
537
537
|
)
|
|
538
538
|
|
|
539
539
|
|
|
540
|
-
def _merge_dict_to(data: Dict[str, Any],
|
|
541
|
-
original = data[
|
|
540
|
+
def _merge_dict_to(data: Dict[str, Any], data_key: str, new: Dict[str, Any]) -> None:
|
|
541
|
+
original = data[data_key] or {}
|
|
542
542
|
for key, value in new.items():
|
|
543
543
|
original[key] = value
|
|
544
|
-
data[
|
|
544
|
+
data[data_key] = original
|
|
545
545
|
|
|
546
546
|
|
|
547
547
|
def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
|
|
@@ -597,7 +597,7 @@ P = TypeVar("P", bound=Parameter)
|
|
|
597
597
|
D = TypeVar("D", bound=dict)
|
|
598
598
|
|
|
599
599
|
|
|
600
|
-
@
|
|
600
|
+
@dataclass
|
|
601
601
|
class OperationDefinition(Generic[P, D]):
|
|
602
602
|
"""A wrapper to store not resolved API operation definitions.
|
|
603
603
|
|
|
@@ -606,10 +606,10 @@ class OperationDefinition(Generic[P, D]):
|
|
|
606
606
|
scope change to have a proper reference resolving later.
|
|
607
607
|
"""
|
|
608
608
|
|
|
609
|
-
raw: D
|
|
610
|
-
resolved: D
|
|
611
|
-
scope: str
|
|
612
|
-
parameters: Sequence[P]
|
|
609
|
+
raw: D
|
|
610
|
+
resolved: D
|
|
611
|
+
scope: str
|
|
612
|
+
parameters: Sequence[P]
|
|
613
613
|
|
|
614
614
|
def __contains__(self, item: Union[str, int]) -> bool:
|
|
615
615
|
return item in self.resolved
|
|
@@ -624,7 +624,7 @@ class OperationDefinition(Generic[P, D]):
|
|
|
624
624
|
C = TypeVar("C", bound=Case)
|
|
625
625
|
|
|
626
626
|
|
|
627
|
-
@
|
|
627
|
+
@dataclass(eq=False)
|
|
628
628
|
class APIOperation(Generic[P, C]):
|
|
629
629
|
"""A single operation defined in an API.
|
|
630
630
|
|
|
@@ -640,23 +640,23 @@ class APIOperation(Generic[P, C]):
|
|
|
640
640
|
# `path` does not contain `basePath`
|
|
641
641
|
# Example <scheme>://<host>/<basePath>/users - "/users" is path
|
|
642
642
|
# https://swagger.io/docs/specification/2-0/api-host-and-base-path/
|
|
643
|
-
path: str
|
|
644
|
-
method: str
|
|
645
|
-
definition: OperationDefinition =
|
|
646
|
-
schema: "BaseSchema"
|
|
647
|
-
verbose_name: str =
|
|
648
|
-
app: Any =
|
|
649
|
-
base_url: Optional[str] =
|
|
650
|
-
path_parameters: ParameterSet[P] =
|
|
651
|
-
headers: ParameterSet[P] =
|
|
652
|
-
cookies: ParameterSet[P] =
|
|
653
|
-
query: ParameterSet[P] =
|
|
654
|
-
body: PayloadAlternatives[P] =
|
|
655
|
-
case_cls: Type[C] =
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
643
|
+
path: str
|
|
644
|
+
method: str
|
|
645
|
+
definition: OperationDefinition = field(repr=False)
|
|
646
|
+
schema: "BaseSchema"
|
|
647
|
+
verbose_name: str = None # type: ignore
|
|
648
|
+
app: Any = None
|
|
649
|
+
base_url: Optional[str] = None
|
|
650
|
+
path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
651
|
+
headers: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
652
|
+
cookies: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
653
|
+
query: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
654
|
+
body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
|
|
655
|
+
case_cls: Type[C] = Case # type: ignore
|
|
656
|
+
|
|
657
|
+
def __post_init__(self) -> None:
|
|
658
|
+
if self.verbose_name is None:
|
|
659
|
+
self.verbose_name = f"{self.method.upper()} {self.full_path}" # type: ignore
|
|
660
660
|
|
|
661
661
|
@property
|
|
662
662
|
def full_path(self) -> str:
|
|
@@ -830,35 +830,35 @@ Endpoint = APIOperation
|
|
|
830
830
|
class Status(str, Enum):
|
|
831
831
|
"""Status of an action or multiple actions."""
|
|
832
832
|
|
|
833
|
-
success = "success"
|
|
834
|
-
failure = "failure"
|
|
835
|
-
error = "error"
|
|
836
|
-
skip = "skip"
|
|
833
|
+
success = "success"
|
|
834
|
+
failure = "failure"
|
|
835
|
+
error = "error"
|
|
836
|
+
skip = "skip"
|
|
837
837
|
|
|
838
838
|
|
|
839
|
-
@
|
|
839
|
+
@dataclass(repr=False)
|
|
840
840
|
class Check:
|
|
841
841
|
"""Single check run result."""
|
|
842
842
|
|
|
843
|
-
name: str
|
|
844
|
-
value: Status
|
|
845
|
-
response: Optional[GenericResponse]
|
|
846
|
-
elapsed: float
|
|
847
|
-
example: Case
|
|
848
|
-
message: Optional[str] =
|
|
843
|
+
name: str
|
|
844
|
+
value: Status
|
|
845
|
+
response: Optional[GenericResponse]
|
|
846
|
+
elapsed: float
|
|
847
|
+
example: Case
|
|
848
|
+
message: Optional[str] = None
|
|
849
849
|
# Failure-specific context
|
|
850
|
-
context: Optional[FailureContext] =
|
|
851
|
-
request: Optional[requests.PreparedRequest] =
|
|
850
|
+
context: Optional[FailureContext] = None
|
|
851
|
+
request: Optional[requests.PreparedRequest] = None
|
|
852
852
|
|
|
853
853
|
|
|
854
|
-
@
|
|
854
|
+
@dataclass(repr=False)
|
|
855
855
|
class Request:
|
|
856
856
|
"""Request data extracted from `Case`."""
|
|
857
857
|
|
|
858
|
-
method: str
|
|
859
|
-
uri: str
|
|
860
|
-
body: Optional[str]
|
|
861
|
-
headers: Headers
|
|
858
|
+
method: str
|
|
859
|
+
uri: str
|
|
860
|
+
body: Optional[str]
|
|
861
|
+
headers: Headers
|
|
862
862
|
|
|
863
863
|
@classmethod
|
|
864
864
|
def from_case(cls, case: Case, session: requests.Session) -> "Request":
|
|
@@ -893,17 +893,17 @@ def serialize_payload(payload: bytes) -> str:
|
|
|
893
893
|
return base64.b64encode(payload).decode()
|
|
894
894
|
|
|
895
895
|
|
|
896
|
-
@
|
|
896
|
+
@dataclass(repr=False)
|
|
897
897
|
class Response:
|
|
898
898
|
"""Unified response data."""
|
|
899
899
|
|
|
900
|
-
status_code: int
|
|
901
|
-
message: str
|
|
902
|
-
headers: Dict[str, List[str]]
|
|
903
|
-
body: Optional[str]
|
|
904
|
-
encoding: Optional[str]
|
|
905
|
-
http_version: str
|
|
906
|
-
elapsed: float
|
|
900
|
+
status_code: int
|
|
901
|
+
message: str
|
|
902
|
+
headers: Dict[str, List[str]]
|
|
903
|
+
body: Optional[str]
|
|
904
|
+
encoding: Optional[str]
|
|
905
|
+
http_version: str
|
|
906
|
+
elapsed: float
|
|
907
907
|
|
|
908
908
|
@classmethod
|
|
909
909
|
def from_requests(cls, response: requests.Response) -> "Response":
|
|
@@ -953,16 +953,16 @@ class Response:
|
|
|
953
953
|
)
|
|
954
954
|
|
|
955
955
|
|
|
956
|
-
@
|
|
956
|
+
@dataclass
|
|
957
957
|
class Interaction:
|
|
958
958
|
"""A single interaction with the target app."""
|
|
959
959
|
|
|
960
|
-
request: Request
|
|
961
|
-
response: Response
|
|
962
|
-
checks: List[Check]
|
|
963
|
-
status: Status
|
|
964
|
-
data_generation_method: DataGenerationMethod
|
|
965
|
-
recorded_at: str =
|
|
960
|
+
request: Request
|
|
961
|
+
response: Response
|
|
962
|
+
checks: List[Check]
|
|
963
|
+
status: Status
|
|
964
|
+
data_generation_method: DataGenerationMethod
|
|
965
|
+
recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
|
|
966
966
|
|
|
967
967
|
@classmethod
|
|
968
968
|
def from_requests(
|
|
@@ -997,28 +997,28 @@ class Interaction:
|
|
|
997
997
|
)
|
|
998
998
|
|
|
999
999
|
|
|
1000
|
-
@
|
|
1000
|
+
@dataclass(repr=False)
|
|
1001
1001
|
class TestResult:
|
|
1002
1002
|
"""Result of a single test."""
|
|
1003
1003
|
|
|
1004
1004
|
__test__ = False
|
|
1005
1005
|
|
|
1006
|
-
method: str
|
|
1007
|
-
path: str
|
|
1008
|
-
verbose_name: str
|
|
1009
|
-
data_generation_method: List[DataGenerationMethod]
|
|
1010
|
-
checks: List[Check] =
|
|
1011
|
-
errors: List[Tuple[Exception, Optional[Case]]] =
|
|
1012
|
-
interactions: List[Interaction] =
|
|
1013
|
-
logs: List[LogRecord] =
|
|
1014
|
-
is_errored: bool =
|
|
1015
|
-
is_flaky: bool =
|
|
1016
|
-
is_skipped: bool =
|
|
1017
|
-
is_executed: bool =
|
|
1018
|
-
seed: Optional[int] =
|
|
1006
|
+
method: str
|
|
1007
|
+
path: str
|
|
1008
|
+
verbose_name: str
|
|
1009
|
+
data_generation_method: List[DataGenerationMethod]
|
|
1010
|
+
checks: List[Check] = field(default_factory=list)
|
|
1011
|
+
errors: List[Tuple[Exception, Optional[Case]]] = field(default_factory=list)
|
|
1012
|
+
interactions: List[Interaction] = field(default_factory=list)
|
|
1013
|
+
logs: List[LogRecord] = field(default_factory=list)
|
|
1014
|
+
is_errored: bool = False
|
|
1015
|
+
is_flaky: bool = False
|
|
1016
|
+
is_skipped: bool = False
|
|
1017
|
+
is_executed: bool = False
|
|
1018
|
+
seed: Optional[int] = None
|
|
1019
1019
|
# To show a proper reproduction code if an error happens and there is no way to get actual headers that were
|
|
1020
1020
|
# sent over the network. Or there could be no actual requests at all
|
|
1021
|
-
overridden_headers: Optional[Dict[str, Any]] =
|
|
1021
|
+
overridden_headers: Optional[Dict[str, Any]] = None
|
|
1022
1022
|
|
|
1023
1023
|
def mark_errored(self) -> None:
|
|
1024
1024
|
self.is_errored = True
|
|
@@ -1094,15 +1094,15 @@ class TestResult:
|
|
|
1094
1094
|
self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
|
|
1095
1095
|
|
|
1096
1096
|
|
|
1097
|
-
@
|
|
1097
|
+
@dataclass(repr=False)
|
|
1098
1098
|
class TestResultSet:
|
|
1099
1099
|
"""Set of multiple test results."""
|
|
1100
1100
|
|
|
1101
1101
|
__test__ = False
|
|
1102
1102
|
|
|
1103
|
-
results: List[TestResult] =
|
|
1104
|
-
generic_errors: List[InvalidSchema] =
|
|
1105
|
-
warnings: List[str] =
|
|
1103
|
+
results: List[TestResult] = field(default_factory=list)
|
|
1104
|
+
generic_errors: List[InvalidSchema] = field(default_factory=list)
|
|
1105
|
+
warnings: List[str] = field(default_factory=list)
|
|
1106
1106
|
|
|
1107
1107
|
def __iter__(self) -> Iterator[TestResult]:
|
|
1108
1108
|
return iter(self.results)
|
|
@@ -1169,4 +1169,4 @@ class TestResultSet:
|
|
|
1169
1169
|
self.warnings.append(warning)
|
|
1170
1170
|
|
|
1171
1171
|
|
|
1172
|
-
CheckFunction = Callable[[GenericResponse, Case], Optional[bool]]
|
|
1172
|
+
CheckFunction = Callable[[GenericResponse, Case], Optional[bool]]
|
schemathesis/parameters.py
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
These are basic entities that describe what data could be sent to the API.
|
|
4
4
|
"""
|
|
5
|
+
from dataclasses import dataclass, field
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, List, Optional, TypeVar
|
|
6
7
|
|
|
7
|
-
import attr
|
|
8
|
-
|
|
9
8
|
if TYPE_CHECKING:
|
|
10
9
|
from .models import APIOperation
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
@
|
|
12
|
+
@dataclass(eq=False)
|
|
14
13
|
class Parameter:
|
|
15
14
|
"""A logically separate parameter bound to a location (e.g., to "query string").
|
|
16
15
|
|
|
@@ -19,7 +18,7 @@ class Parameter:
|
|
|
19
18
|
"""
|
|
20
19
|
|
|
21
20
|
# The parameter definition in the language acceptable by the API
|
|
22
|
-
definition: Any
|
|
21
|
+
definition: Any
|
|
23
22
|
|
|
24
23
|
@property
|
|
25
24
|
def location(self) -> str:
|
|
@@ -52,11 +51,11 @@ class Parameter:
|
|
|
52
51
|
P = TypeVar("P", bound=Parameter)
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
@
|
|
54
|
+
@dataclass
|
|
56
55
|
class ParameterSet(Generic[P]):
|
|
57
56
|
"""A set of parameters for the same location."""
|
|
58
57
|
|
|
59
|
-
items: List[P] =
|
|
58
|
+
items: List[P] = field(default_factory=list)
|
|
60
59
|
|
|
61
60
|
def add(self, parameter: P) -> None:
|
|
62
61
|
"""Add a new parameter."""
|