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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Expression nodes description and evaluation logic."""
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum, unique
|
|
3
4
|
from typing import Any, Dict, Optional, Union
|
|
4
5
|
|
|
5
|
-
import attr
|
|
6
6
|
from requests.structures import CaseInsensitiveDict
|
|
7
7
|
|
|
8
8
|
from ....utils import WSGIResponse
|
|
@@ -10,7 +10,7 @@ from .. import references
|
|
|
10
10
|
from .context import ExpressionContext
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@dataclass
|
|
14
14
|
class Node:
|
|
15
15
|
"""Generic expression node."""
|
|
16
16
|
|
|
@@ -27,11 +27,11 @@ class NodeType(Enum):
|
|
|
27
27
|
RESPONSE = "$response"
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
@
|
|
30
|
+
@dataclass
|
|
31
31
|
class String(Node):
|
|
32
32
|
"""A simple string that is not evaluated somehow specifically."""
|
|
33
33
|
|
|
34
|
-
value: str
|
|
34
|
+
value: str
|
|
35
35
|
|
|
36
36
|
def evaluate(self, context: ExpressionContext) -> str:
|
|
37
37
|
"""String tokens are passed as they are.
|
|
@@ -43,7 +43,7 @@ class String(Node):
|
|
|
43
43
|
return self.value
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@
|
|
46
|
+
@dataclass
|
|
47
47
|
class URL(Node):
|
|
48
48
|
"""A node for `$url` expression."""
|
|
49
49
|
|
|
@@ -51,7 +51,7 @@ class URL(Node):
|
|
|
51
51
|
return context.case.get_full_url()
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
@
|
|
54
|
+
@dataclass
|
|
55
55
|
class Method(Node):
|
|
56
56
|
"""A node for `$method` expression."""
|
|
57
57
|
|
|
@@ -59,7 +59,7 @@ class Method(Node):
|
|
|
59
59
|
return context.case.operation.method.upper()
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
@
|
|
62
|
+
@dataclass
|
|
63
63
|
class StatusCode(Node):
|
|
64
64
|
"""A node for `$statusCode` expression."""
|
|
65
65
|
|
|
@@ -67,12 +67,12 @@ class StatusCode(Node):
|
|
|
67
67
|
return str(context.response.status_code)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
@
|
|
70
|
+
@dataclass
|
|
71
71
|
class NonBodyRequest(Node):
|
|
72
72
|
"""A node for `$request` expressions where location is not `body`."""
|
|
73
73
|
|
|
74
|
-
location: str
|
|
75
|
-
parameter: str
|
|
74
|
+
location: str
|
|
75
|
+
parameter: str
|
|
76
76
|
|
|
77
77
|
def evaluate(self, context: ExpressionContext) -> str:
|
|
78
78
|
container: Union[Dict, CaseInsensitiveDict] = {
|
|
@@ -85,11 +85,11 @@ class NonBodyRequest(Node):
|
|
|
85
85
|
return container[self.parameter]
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
@
|
|
88
|
+
@dataclass
|
|
89
89
|
class BodyRequest(Node):
|
|
90
90
|
"""A node for `$request` expressions where location is `body`."""
|
|
91
91
|
|
|
92
|
-
pointer: Optional[str] =
|
|
92
|
+
pointer: Optional[str] = None
|
|
93
93
|
|
|
94
94
|
def evaluate(self, context: ExpressionContext) -> Any:
|
|
95
95
|
document = context.case.body
|
|
@@ -98,21 +98,21 @@ class BodyRequest(Node):
|
|
|
98
98
|
return references.resolve_pointer(document, self.pointer[1:])
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
@
|
|
101
|
+
@dataclass
|
|
102
102
|
class HeaderResponse(Node):
|
|
103
103
|
"""A node for `$response.header` expressions."""
|
|
104
104
|
|
|
105
|
-
parameter: str
|
|
105
|
+
parameter: str
|
|
106
106
|
|
|
107
107
|
def evaluate(self, context: ExpressionContext) -> str:
|
|
108
108
|
return context.response.headers[self.parameter]
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
@
|
|
111
|
+
@dataclass
|
|
112
112
|
class BodyResponse(Node):
|
|
113
113
|
"""A node for `$response.body` expressions."""
|
|
114
114
|
|
|
115
|
-
pointer: Optional[str] =
|
|
115
|
+
pointer: Optional[str] = None
|
|
116
116
|
|
|
117
117
|
def evaluate(self, context: ExpressionContext) -> Any:
|
|
118
118
|
if isinstance(context.response, WSGIResponse):
|
|
@@ -5,7 +5,7 @@ from . import lexer, nodes
|
|
|
5
5
|
from .errors import RuntimeExpressionError, UnknownToken
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@lru_cache()
|
|
8
|
+
@lru_cache()
|
|
9
9
|
def parse(expr: str) -> List[nodes.Node]:
|
|
10
10
|
"""Parse lexical tokens into concrete expression nodes."""
|
|
11
11
|
return list(_parse(expr))
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Based on https://swagger.io/docs/specification/links/
|
|
4
4
|
"""
|
|
5
|
+
from dataclasses import dataclass, field
|
|
5
6
|
from difflib import get_close_matches
|
|
6
7
|
from typing import Any, Dict, Generator, List, NoReturn, Optional, Sequence, Tuple, Union
|
|
7
8
|
|
|
8
|
-
import attr
|
|
9
|
-
|
|
10
9
|
from ...models import APIOperation, Case
|
|
11
10
|
from ...parameters import ParameterSet
|
|
12
11
|
from ...stateful import Direction, ParsedData, StatefulTest
|
|
@@ -17,15 +16,14 @@ from .constants import LOCATION_TO_CONTAINER
|
|
|
17
16
|
from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
@
|
|
19
|
+
@dataclass(repr=False)
|
|
21
20
|
class Link(StatefulTest):
|
|
22
|
-
operation: APIOperation
|
|
23
|
-
parameters: Dict[str, Any]
|
|
24
|
-
request_body: Any =
|
|
21
|
+
operation: APIOperation
|
|
22
|
+
parameters: Dict[str, Any]
|
|
23
|
+
request_body: Any = NOT_SET
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if value is not NOT_SET and not self.operation.body:
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if self.request_body is not NOT_SET and not self.operation.body:
|
|
29
27
|
# Link defines `requestBody` for a parameter that does not accept one
|
|
30
28
|
raise ValueError(
|
|
31
29
|
f"Request body is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
|
|
@@ -159,21 +157,21 @@ def get_links(response: GenericResponse, operation: APIOperation, field: str) ->
|
|
|
159
157
|
return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
|
|
160
158
|
|
|
161
159
|
|
|
162
|
-
@
|
|
160
|
+
@dataclass(repr=False)
|
|
163
161
|
class OpenAPILink(Direction):
|
|
164
162
|
"""Alternative approach to link processing.
|
|
165
163
|
|
|
166
164
|
NOTE. This class will replace `Link` in the future.
|
|
167
165
|
"""
|
|
168
166
|
|
|
169
|
-
name: str
|
|
170
|
-
status_code: str
|
|
171
|
-
definition: Dict[str, Any]
|
|
172
|
-
operation: APIOperation
|
|
173
|
-
parameters: List[Tuple[Optional[str], str, str]] =
|
|
174
|
-
body: Union[Dict[str, Any], NotSet] =
|
|
167
|
+
name: str
|
|
168
|
+
status_code: str
|
|
169
|
+
definition: Dict[str, Any]
|
|
170
|
+
operation: APIOperation
|
|
171
|
+
parameters: List[Tuple[Optional[str], str, str]] = field(init=False)
|
|
172
|
+
body: Union[Dict[str, Any], NotSet] = field(init=False)
|
|
175
173
|
|
|
176
|
-
def
|
|
174
|
+
def __post_init__(self) -> None:
|
|
177
175
|
self.parameters = [
|
|
178
176
|
normalize_parameter(parameter, expression)
|
|
179
177
|
for parameter, expression in self.definition.get("parameters", {}).items()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from functools import lru_cache
|
|
2
3
|
from typing import Any, Dict, Optional, Tuple
|
|
3
4
|
from urllib.parse import urlencode
|
|
4
5
|
|
|
5
|
-
import attr
|
|
6
6
|
import jsonschema
|
|
7
7
|
from hypothesis import strategies as st
|
|
8
8
|
from hypothesis_jsonschema import from_schema
|
|
@@ -12,16 +12,16 @@ from .mutations import MutationContext
|
|
|
12
12
|
from .types import Draw, Schema
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@
|
|
15
|
+
@dataclass
|
|
16
16
|
class CacheKey:
|
|
17
17
|
"""A cache key for API Operation / location.
|
|
18
18
|
|
|
19
19
|
Carries the schema around but don't use it for hashing to simplify LRU cache usage.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
operation_name: str
|
|
23
|
-
location: str
|
|
24
|
-
schema: Schema
|
|
22
|
+
operation_name: str
|
|
23
|
+
location: str
|
|
24
|
+
schema: Schema
|
|
25
25
|
|
|
26
26
|
def __hash__(self) -> int:
|
|
27
27
|
return hash((self.operation_name, self.location))
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Schema mutations."""
|
|
2
2
|
import enum
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from functools import wraps
|
|
4
5
|
from typing import Any, Callable, List, Optional, Sequence, Set, Tuple, TypeVar
|
|
5
6
|
|
|
6
|
-
import attr
|
|
7
7
|
from hypothesis import reject
|
|
8
8
|
from hypothesis import strategies as st
|
|
9
9
|
from hypothesis.strategies._internal.featureflags import FeatureStrategy
|
|
@@ -58,17 +58,17 @@ TYPE_SPECIFIC_KEYS = {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
@
|
|
61
|
+
@dataclass
|
|
62
62
|
class MutationContext:
|
|
63
63
|
"""Meta information about the current mutation state."""
|
|
64
64
|
|
|
65
65
|
# The original schema
|
|
66
|
-
keywords: Schema
|
|
67
|
-
non_keywords: Schema
|
|
66
|
+
keywords: Schema # only keywords
|
|
67
|
+
non_keywords: Schema # everything else
|
|
68
68
|
# Schema location within API operation (header, query, etc)
|
|
69
|
-
location: str
|
|
69
|
+
location: str
|
|
70
70
|
# Payload media type, if available
|
|
71
|
-
media_type: Optional[str]
|
|
71
|
+
media_type: Optional[str]
|
|
72
72
|
|
|
73
73
|
@property
|
|
74
74
|
def is_header_location(self) -> bool:
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Tuple
|
|
3
4
|
|
|
4
|
-
import attr
|
|
5
|
-
|
|
6
5
|
from ...exceptions import InvalidSchema
|
|
7
6
|
from ...models import APIOperation
|
|
8
7
|
from ...parameters import Parameter
|
|
9
8
|
from .converter import to_json_schema_recursive
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
@
|
|
11
|
+
@dataclass(eq=False)
|
|
13
12
|
class OpenAPIParameter(Parameter):
|
|
14
13
|
"""A single Open API operation parameter."""
|
|
15
14
|
|
|
@@ -122,7 +121,7 @@ class OpenAPIParameter(Parameter):
|
|
|
122
121
|
return json.dumps(self.as_json_schema(operation), sort_keys=True)
|
|
123
122
|
|
|
124
123
|
|
|
125
|
-
@
|
|
124
|
+
@dataclass(eq=False)
|
|
126
125
|
class OpenAPI20Parameter(OpenAPIParameter):
|
|
127
126
|
"""Open API 2.0 parameter.
|
|
128
127
|
|
|
@@ -167,7 +166,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
|
|
|
167
166
|
return None
|
|
168
167
|
|
|
169
168
|
|
|
170
|
-
@
|
|
169
|
+
@dataclass(eq=False)
|
|
171
170
|
class OpenAPI30Parameter(OpenAPIParameter):
|
|
172
171
|
"""Open API 3.0 parameter.
|
|
173
172
|
|
|
@@ -217,9 +216,9 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
|
217
216
|
return super().from_open_api_to_json_schema(operation, open_api_schema)
|
|
218
217
|
|
|
219
218
|
|
|
220
|
-
@
|
|
219
|
+
@dataclass(eq=False)
|
|
221
220
|
class OpenAPIBody(OpenAPIParameter):
|
|
222
|
-
media_type: str
|
|
221
|
+
media_type: str
|
|
223
222
|
|
|
224
223
|
@property
|
|
225
224
|
def location(self) -> str:
|
|
@@ -231,7 +230,7 @@ class OpenAPIBody(OpenAPIParameter):
|
|
|
231
230
|
return "body"
|
|
232
231
|
|
|
233
232
|
|
|
234
|
-
@
|
|
233
|
+
@dataclass(eq=False)
|
|
235
234
|
class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
|
|
236
235
|
"""Open API 2.0 body variant."""
|
|
237
236
|
|
|
@@ -280,7 +279,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
|
|
|
280
279
|
FORM_MEDIA_TYPES = ("multipart/form-data", "application/x-www-form-urlencoded")
|
|
281
280
|
|
|
282
281
|
|
|
283
|
-
@
|
|
282
|
+
@dataclass(eq=False)
|
|
284
283
|
class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
|
|
285
284
|
"""Open API 3.0 body variant.
|
|
286
285
|
|
|
@@ -290,8 +289,8 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
|
|
|
290
289
|
|
|
291
290
|
# The `required` keyword is located above the schema for concrete media-type;
|
|
292
291
|
# Therefore, it is passed here explicitly
|
|
293
|
-
required: bool =
|
|
294
|
-
description: Optional[str] =
|
|
292
|
+
required: bool = False
|
|
293
|
+
description: Optional[str] = None
|
|
295
294
|
|
|
296
295
|
def as_json_schema(self, operation: APIOperation) -> Dict[str, Any]:
|
|
297
296
|
"""Convert body definition to JSON Schema."""
|
|
@@ -315,11 +314,11 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
|
|
|
315
314
|
return self.required
|
|
316
315
|
|
|
317
316
|
|
|
318
|
-
@
|
|
317
|
+
@dataclass(eq=False)
|
|
319
318
|
class OpenAPI20CompositeBody(OpenAPIBody, OpenAPI20Parameter):
|
|
320
319
|
"""A special container to abstract over multiple `formData` parameters."""
|
|
321
320
|
|
|
322
|
-
definition: List[OpenAPI20Parameter]
|
|
321
|
+
definition: List[OpenAPI20Parameter]
|
|
323
322
|
|
|
324
323
|
@classmethod
|
|
325
324
|
def from_parameters(cls, *parameters: Dict[str, Any], media_type: str) -> "OpenAPI20CompositeBody":
|
|
@@ -52,11 +52,11 @@ class InliningResolver(jsonschema.RefResolver):
|
|
|
52
52
|
)
|
|
53
53
|
super().__init__(*args, **kwargs)
|
|
54
54
|
|
|
55
|
-
@overload
|
|
55
|
+
@overload
|
|
56
56
|
def resolve_all(self, item: Dict[str, Any], recursion_level: int = 0) -> Dict[str, Any]:
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
|
-
@overload
|
|
59
|
+
@overload
|
|
60
60
|
def resolve_all(self, item: List, recursion_level: int = 0) -> List:
|
|
61
61
|
pass
|
|
62
62
|
|
|
@@ -2,6 +2,7 @@ import itertools
|
|
|
2
2
|
import json
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from contextlib import ExitStack, contextmanager
|
|
5
|
+
from dataclasses import dataclass, field
|
|
5
6
|
from difflib import get_close_matches
|
|
6
7
|
from hashlib import sha1
|
|
7
8
|
from json import JSONDecodeError
|
|
@@ -23,7 +24,6 @@ from typing import (
|
|
|
23
24
|
)
|
|
24
25
|
from urllib.parse import urlsplit
|
|
25
26
|
|
|
26
|
-
import attr
|
|
27
27
|
import jsonschema
|
|
28
28
|
import requests
|
|
29
29
|
from hypothesis.strategies import SearchStrategy
|
|
@@ -82,24 +82,20 @@ SCHEMA_ERROR_MESSAGE = "Schema parsing failed. Please check your schema."
|
|
|
82
82
|
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
@
|
|
85
|
+
@dataclass(eq=False, repr=False)
|
|
86
86
|
class BaseOpenAPISchema(BaseSchema):
|
|
87
|
-
nullable_name: str
|
|
88
|
-
links_field: str
|
|
89
|
-
header_required_field: str
|
|
90
|
-
security: BaseSecurityProcessor
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
_inline_reference_cache: Dict[str, Any]
|
|
87
|
+
nullable_name: ClassVar[str] = ""
|
|
88
|
+
links_field: ClassVar[str] = ""
|
|
89
|
+
header_required_field: ClassVar[str] = ""
|
|
90
|
+
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
|
91
|
+
_operations_by_id: Dict[str, APIOperation] = field(init=False)
|
|
92
|
+
_inline_reference_cache: Dict[str, Any] = field(default_factory=dict)
|
|
94
93
|
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
|
95
94
|
# excessive resolving
|
|
96
|
-
_inline_reference_cache_lock: RLock
|
|
97
|
-
|
|
98
|
-
def __attrs_post_init__(self) -> None:
|
|
99
|
-
self._inline_reference_cache = {}
|
|
100
|
-
self._inline_reference_cache_lock = RLock()
|
|
95
|
+
_inline_reference_cache_lock: RLock = field(default_factory=RLock)
|
|
96
|
+
component_locations: ClassVar[Tuple[Tuple[str, ...], ...]] = ()
|
|
101
97
|
|
|
102
|
-
@property
|
|
98
|
+
@property
|
|
103
99
|
def spec_version(self) -> str:
|
|
104
100
|
raise NotImplementedError
|
|
105
101
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""Processing of ``securityDefinitions`` or ``securitySchemes`` keywords."""
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from typing import Any, ClassVar, Dict, Generator, List, Tuple, Type
|
|
3
4
|
|
|
4
|
-
import attr
|
|
5
5
|
from jsonschema import RefResolver
|
|
6
6
|
|
|
7
7
|
from ...models import APIOperation
|
|
8
8
|
from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
@
|
|
11
|
+
@dataclass
|
|
12
12
|
class BaseSecurityProcessor:
|
|
13
|
-
api_key_locations: Tuple[str, ...] = ("header", "query")
|
|
14
|
-
http_security_name = "basic"
|
|
13
|
+
api_key_locations: ClassVar[Tuple[str, ...]] = ("header", "query")
|
|
14
|
+
http_security_name: ClassVar[str] = "basic"
|
|
15
15
|
parameter_cls: ClassVar[Type[OpenAPIParameter]] = OpenAPI20Parameter
|
|
16
16
|
|
|
17
17
|
def process_definitions(self, schema: Dict[str, Any], operation: APIOperation, resolver: RefResolver) -> None:
|
|
@@ -112,10 +112,10 @@ def make_api_key_schema(definition: Dict[str, Any], **kwargs: Any) -> Dict[str,
|
|
|
112
112
|
SwaggerSecurityProcessor = BaseSecurityProcessor
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
@
|
|
115
|
+
@dataclass
|
|
116
116
|
class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
117
|
-
api_key_locations = ("header", "cookie", "query")
|
|
118
|
-
http_security_name = "http"
|
|
117
|
+
api_key_locations: ClassVar[Tuple[str, ...]] = ("header", "cookie", "query")
|
|
118
|
+
http_security_name: ClassVar[str] = "http"
|
|
119
119
|
parameter_cls: ClassVar[Type[OpenAPIParameter]] = OpenAPI30Parameter
|
|
120
120
|
|
|
121
121
|
def get_security_definitions(self, schema: Dict[str, Any], resolver: RefResolver) -> Dict[str, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from typing import TYPE_CHECKING, Callable, Dict, List, Tuple
|
|
2
3
|
|
|
3
|
-
import attr
|
|
4
4
|
import hypothesis.strategies as st
|
|
5
5
|
from requests.structures import CaseInsensitiveDict
|
|
6
6
|
|
|
@@ -14,10 +14,10 @@ if TYPE_CHECKING:
|
|
|
14
14
|
FilterFunction = Callable[[StepResult], bool]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
@
|
|
17
|
+
@dataclass
|
|
18
18
|
class Connection:
|
|
19
|
-
source: str
|
|
20
|
-
strategy: st.SearchStrategy[Tuple[StepResult, OpenAPILink]]
|
|
19
|
+
source: str
|
|
20
|
+
strategy: st.SearchStrategy[Tuple[StepResult, OpenAPILink]]
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
APIOperationConnections = Dict[str, List[Connection]]
|
schemathesis/stateful.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import json
|
|
3
3
|
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type
|
|
5
6
|
|
|
6
|
-
import attr
|
|
7
7
|
import hypothesis
|
|
8
8
|
from hypothesis.stateful import RuleBasedStateMachine
|
|
9
9
|
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
|
@@ -25,15 +25,15 @@ class Stateful(enum.Enum):
|
|
|
25
25
|
links = 2
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@
|
|
28
|
+
@dataclass
|
|
29
29
|
class ParsedData:
|
|
30
30
|
"""A structure that holds information parsed from a test outcome.
|
|
31
31
|
|
|
32
32
|
It is used later to create a new version of an API operation that will reuse this data.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
parameters: Dict[str, Any]
|
|
36
|
-
body: Any =
|
|
35
|
+
parameters: Dict[str, Any]
|
|
36
|
+
body: Any = NOT_SET
|
|
37
37
|
|
|
38
38
|
def __hash__(self) -> int:
|
|
39
39
|
"""Custom hash simplifies deduplication of parsed data."""
|
|
@@ -48,11 +48,11 @@ class ParsedData:
|
|
|
48
48
|
return value
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
@
|
|
51
|
+
@dataclass
|
|
52
52
|
class StatefulTest:
|
|
53
53
|
"""A template for a test that will be executed after another one by reusing the outcomes from it."""
|
|
54
54
|
|
|
55
|
-
name: str
|
|
55
|
+
name: str
|
|
56
56
|
|
|
57
57
|
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
|
|
58
58
|
raise NotImplementedError
|
|
@@ -61,12 +61,12 @@ class StatefulTest:
|
|
|
61
61
|
raise NotImplementedError
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
@
|
|
64
|
+
@dataclass
|
|
65
65
|
class StatefulData:
|
|
66
66
|
"""Storage for data that will be used in later tests."""
|
|
67
67
|
|
|
68
|
-
stateful_test: StatefulTest
|
|
69
|
-
container: List[ParsedData] =
|
|
68
|
+
stateful_test: StatefulTest
|
|
69
|
+
container: List[ParsedData] = field(default_factory=list)
|
|
70
70
|
|
|
71
71
|
def make_operation(self) -> APIOperation:
|
|
72
72
|
return self.stateful_test.make_operation(self.container)
|
|
@@ -77,16 +77,16 @@ class StatefulData:
|
|
|
77
77
|
self.container.append(parsed)
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
@
|
|
80
|
+
@dataclass
|
|
81
81
|
class Feedback:
|
|
82
82
|
"""Handler for feedback from tests.
|
|
83
83
|
|
|
84
84
|
Provides a way to control runner's behavior from tests.
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
-
stateful: Optional[Stateful]
|
|
88
|
-
operation: APIOperation =
|
|
89
|
-
stateful_tests: Dict[str, StatefulData] =
|
|
87
|
+
stateful: Optional[Stateful]
|
|
88
|
+
operation: APIOperation = field(repr=False)
|
|
89
|
+
stateful_tests: Dict[str, StatefulData] = field(default_factory=dict, repr=False)
|
|
90
90
|
|
|
91
91
|
def add_test_case(self, case: Case, response: GenericResponse) -> None:
|
|
92
92
|
"""Store test data to reuse it in the future additional tests."""
|
|
@@ -117,13 +117,13 @@ class Feedback:
|
|
|
117
117
|
yield Ok((operation, test_function))
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
@
|
|
120
|
+
@dataclass
|
|
121
121
|
class StepResult:
|
|
122
122
|
"""Output from a single transition of a state machine."""
|
|
123
123
|
|
|
124
|
-
response: GenericResponse
|
|
125
|
-
case: Case
|
|
126
|
-
elapsed: float
|
|
124
|
+
response: GenericResponse
|
|
125
|
+
case: Case
|
|
126
|
+
elapsed: float
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
class Direction:
|
|
@@ -148,11 +148,11 @@ def _print_case(case: Case, kwargs: Dict[str, Any]) -> str:
|
|
|
148
148
|
return f"{operation}.make_case({', '.join(data)})"
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
@
|
|
151
|
+
@dataclass(repr=False)
|
|
152
152
|
class _DirectionWrapper:
|
|
153
153
|
"""Purely to avoid modification of `Direction.__repr__`."""
|
|
154
154
|
|
|
155
|
-
direction: Direction
|
|
155
|
+
direction: Direction
|
|
156
156
|
|
|
157
157
|
def __repr__(self) -> str:
|
|
158
158
|
path = self.direction.operation.path
|
schemathesis/targets.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from typing import TYPE_CHECKING, Callable, Tuple
|
|
2
3
|
|
|
3
|
-
import attr
|
|
4
|
-
|
|
5
4
|
from .utils import GenericResponse
|
|
6
5
|
|
|
7
6
|
if TYPE_CHECKING:
|
|
8
7
|
from .models import Case
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
@
|
|
10
|
+
@dataclass
|
|
12
11
|
class TargetContext:
|
|
13
12
|
"""Context for targeted testing.
|
|
14
13
|
|
|
@@ -17,9 +16,9 @@ class TargetContext:
|
|
|
17
16
|
:ivar float response_time: API response time.
|
|
18
17
|
"""
|
|
19
18
|
|
|
20
|
-
case: "Case"
|
|
21
|
-
response: GenericResponse
|
|
22
|
-
response_time: float
|
|
19
|
+
case: "Case"
|
|
20
|
+
response: GenericResponse
|
|
21
|
+
response_time: float
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def response_time(context: TargetContext) -> float:
|
schemathesis/types.py
CHANGED
|
@@ -7,15 +7,15 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from . import DataGenerationMethod
|
|
8
8
|
from .hooks import HookContext
|
|
9
9
|
|
|
10
|
-
PathLike = Union[Path, str]
|
|
10
|
+
PathLike = Union[Path, str]
|
|
11
11
|
|
|
12
|
-
Query = Dict[str, Any]
|
|
12
|
+
Query = Dict[str, Any]
|
|
13
13
|
# Body can be of any Python type that corresponds to JSON Schema types + `bytes`
|
|
14
|
-
Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
|
|
15
|
-
PathParameters = Dict[str, Any]
|
|
16
|
-
Headers = Dict[str, Any]
|
|
17
|
-
Cookies = Dict[str, Any]
|
|
18
|
-
FormData = Dict[str, Any]
|
|
14
|
+
Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
|
|
15
|
+
PathParameters = Dict[str, Any]
|
|
16
|
+
Headers = Dict[str, Any]
|
|
17
|
+
Cookies = Dict[str, Any]
|
|
18
|
+
FormData = Dict[str, Any]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class NotSet:
|
|
@@ -26,13 +26,11 @@ RequestCert = Union[str, Tuple[str, str]]
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# A filter for path / method
|
|
29
|
-
Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
|
|
29
|
+
Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
|
|
30
30
|
|
|
31
|
-
Hook = Union[
|
|
32
|
-
Callable[[SearchStrategy], SearchStrategy], Callable[[SearchStrategy, "HookContext"], SearchStrategy]
|
|
33
|
-
] # pragma: no mutate
|
|
31
|
+
Hook = Union[Callable[[SearchStrategy], SearchStrategy], Callable[[SearchStrategy, "HookContext"], SearchStrategy]]
|
|
34
32
|
|
|
35
|
-
RawAuth = Tuple[str, str]
|
|
33
|
+
RawAuth = Tuple[str, str]
|
|
36
34
|
# Generic test with any arguments and no return
|
|
37
|
-
GenericTest = Callable[..., None]
|
|
35
|
+
GenericTest = Callable[..., None]
|
|
38
36
|
DataGenerationMethodInput = Union["DataGenerationMethod", Iterable["DataGenerationMethod"]]
|
schemathesis/utils.py
CHANGED
|
@@ -80,7 +80,7 @@ def is_latin_1_encodable(value: str) -> bool:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
# Adapted from http.client._is_illegal_header_value
|
|
83
|
-
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
|
83
|
+
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def has_invalid_characters(name: str, value: str) -> bool:
|
|
@@ -258,7 +258,7 @@ def get_requests_auth(auth: Optional[RawAuth], auth_type: Optional[str]) -> Opti
|
|
|
258
258
|
return auth
|
|
259
259
|
|
|
260
260
|
|
|
261
|
-
GenericResponse = Union[requests.Response, WSGIResponse]
|
|
261
|
+
GenericResponse = Union[requests.Response, WSGIResponse]
|
|
262
262
|
|
|
263
263
|
|
|
264
264
|
def copy_response(response: GenericResponse) -> GenericResponse:
|