schemathesis 3.18.5__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/__init__.py +1 -3
- schemathesis/auths.py +218 -43
- schemathesis/cli/__init__.py +37 -20
- schemathesis/cli/callbacks.py +13 -1
- schemathesis/cli/cassettes.py +18 -18
- schemathesis/cli/context.py +25 -24
- schemathesis/cli/debug.py +3 -3
- schemathesis/cli/junitxml.py +4 -4
- schemathesis/cli/options.py +1 -1
- schemathesis/cli/output/default.py +2 -0
- 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 +269 -0
- schemathesis/hooks.py +11 -11
- schemathesis/lazy.py +21 -16
- schemathesis/models.py +149 -107
- schemathesis/parameters.py +12 -7
- 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 +38 -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/loaders.py +21 -2
- 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/loaders.py +29 -2
- 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 +12 -7
- schemathesis/specs/openapi/stateful/links.py +4 -4
- schemathesis/stateful.py +19 -19
- schemathesis/targets.py +5 -6
- schemathesis/throttling.py +34 -0
- schemathesis/types.py +11 -13
- schemathesis/utils.py +2 -2
- {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/METADATA +4 -3
- schemathesis-3.19.1.dist-info/RECORD +107 -0
- schemathesis-3.18.5.dist-info/RECORD +0 -105
- {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.18.5.dist-info → schemathesis-3.19.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
from dataclasses import dataclass
|
|
2
2
|
|
|
3
3
|
from ....models import Case
|
|
4
4
|
from ....utils import GenericResponse
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
@
|
|
7
|
+
@dataclass
|
|
8
8
|
class ExpressionContext:
|
|
9
9
|
"""Context in what an expression are evaluated."""
|
|
10
10
|
|
|
11
|
-
response: GenericResponse
|
|
12
|
-
case: Case
|
|
11
|
+
response: GenericResponse
|
|
12
|
+
case: Case
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
"""Lexical analysis of runtime expressions."""
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum, unique
|
|
3
4
|
from typing import Callable, Generator
|
|
4
5
|
|
|
5
|
-
import attr
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
@unique # pragma: no mutate
|
|
7
|
+
@unique
|
|
9
8
|
class TokenType(Enum):
|
|
10
|
-
VARIABLE = 1
|
|
11
|
-
STRING = 2
|
|
12
|
-
POINTER = 3
|
|
13
|
-
DOT = 4
|
|
14
|
-
LBRACKET = 5
|
|
15
|
-
RBRACKET = 6
|
|
9
|
+
VARIABLE = 1
|
|
10
|
+
STRING = 2
|
|
11
|
+
POINTER = 3
|
|
12
|
+
DOT = 4
|
|
13
|
+
LBRACKET = 5
|
|
14
|
+
RBRACKET = 6
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
@
|
|
17
|
+
@dataclass
|
|
19
18
|
class Token:
|
|
20
19
|
"""Lexical token that may occur in a runtime expression."""
|
|
21
20
|
|
|
22
|
-
value: str
|
|
23
|
-
type_: TokenType
|
|
21
|
+
value: str
|
|
22
|
+
type_: TokenType
|
|
24
23
|
|
|
25
24
|
# Helpers for cleaner instantiation
|
|
26
25
|
|
|
@@ -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()
|
|
@@ -9,6 +9,7 @@ import jsonschema
|
|
|
9
9
|
import requests
|
|
10
10
|
import yaml
|
|
11
11
|
from jsonschema import ValidationError
|
|
12
|
+
from pyrate_limiter import Limiter
|
|
12
13
|
from starlette.applications import Starlette
|
|
13
14
|
from starlette_testclient import TestClient as ASGIClient
|
|
14
15
|
from werkzeug.test import Client
|
|
@@ -18,6 +19,7 @@ from ...constants import DEFAULT_DATA_GENERATION_METHODS, WAIT_FOR_SCHEMA_INTERV
|
|
|
18
19
|
from ...exceptions import HTTPError, SchemaLoadingError
|
|
19
20
|
from ...hooks import HookContext, dispatch
|
|
20
21
|
from ...lazy import LazySchema
|
|
22
|
+
from ...throttling import build_limiter
|
|
21
23
|
from ...types import DataGenerationMethodInput, Filter, NotSet, PathLike
|
|
22
24
|
from ...utils import (
|
|
23
25
|
NOT_SET,
|
|
@@ -61,6 +63,7 @@ def from_path(
|
|
|
61
63
|
force_schema_version: Optional[str] = None,
|
|
62
64
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
63
65
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
66
|
+
rate_limit: Optional[str] = None,
|
|
64
67
|
encoding: str = "utf8",
|
|
65
68
|
) -> BaseOpenAPISchema:
|
|
66
69
|
"""Load Open API schema via a file from an OS path.
|
|
@@ -83,6 +86,7 @@ def from_path(
|
|
|
83
86
|
data_generation_methods=data_generation_methods,
|
|
84
87
|
code_sample_style=code_sample_style,
|
|
85
88
|
location=pathlib.Path(path).absolute().as_uri(),
|
|
89
|
+
rate_limit=rate_limit,
|
|
86
90
|
__expects_json=_is_json_path(path),
|
|
87
91
|
)
|
|
88
92
|
|
|
@@ -103,6 +107,7 @@ def from_uri(
|
|
|
103
107
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
104
108
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
105
109
|
wait_for_schema: Optional[float] = None,
|
|
110
|
+
rate_limit: Optional[str] = None,
|
|
106
111
|
**kwargs: Any,
|
|
107
112
|
) -> BaseOpenAPISchema:
|
|
108
113
|
"""Load Open API schema from the network.
|
|
@@ -110,8 +115,10 @@ def from_uri(
|
|
|
110
115
|
:param str uri: Schema URL.
|
|
111
116
|
"""
|
|
112
117
|
setup_headers(kwargs)
|
|
113
|
-
if
|
|
114
|
-
|
|
118
|
+
if port:
|
|
119
|
+
uri = str(URL(uri).with_port(port))
|
|
120
|
+
if not base_url:
|
|
121
|
+
base_url = uri
|
|
115
122
|
|
|
116
123
|
if wait_for_schema is not None:
|
|
117
124
|
|
|
@@ -144,6 +151,7 @@ def from_uri(
|
|
|
144
151
|
data_generation_methods=data_generation_methods,
|
|
145
152
|
code_sample_style=code_sample_style,
|
|
146
153
|
location=uri,
|
|
154
|
+
rate_limit=rate_limit,
|
|
147
155
|
__expects_json=_is_json_response(response),
|
|
148
156
|
)
|
|
149
157
|
except SchemaLoadingError as exc:
|
|
@@ -181,6 +189,7 @@ def from_file(
|
|
|
181
189
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
182
190
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
183
191
|
location: Optional[str] = None,
|
|
192
|
+
rate_limit: Optional[str] = None,
|
|
184
193
|
__expects_json: bool = False,
|
|
185
194
|
**kwargs: Any, # needed in the runner to have compatible API across all loaders
|
|
186
195
|
) -> BaseOpenAPISchema:
|
|
@@ -216,6 +225,7 @@ def from_file(
|
|
|
216
225
|
data_generation_methods=data_generation_methods,
|
|
217
226
|
code_sample_style=code_sample_style,
|
|
218
227
|
location=location,
|
|
228
|
+
rate_limit=rate_limit,
|
|
219
229
|
)
|
|
220
230
|
|
|
221
231
|
|
|
@@ -234,6 +244,7 @@ def from_dict(
|
|
|
234
244
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
235
245
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
236
246
|
location: Optional[str] = None,
|
|
247
|
+
rate_limit: Optional[str] = None,
|
|
237
248
|
) -> BaseOpenAPISchema:
|
|
238
249
|
"""Load Open API schema from a Python dictionary.
|
|
239
250
|
|
|
@@ -242,6 +253,9 @@ def from_dict(
|
|
|
242
253
|
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
|
243
254
|
hook_context = HookContext()
|
|
244
255
|
dispatch("before_load_schema", hook_context, raw_schema)
|
|
256
|
+
rate_limiter: Optional[Limiter] = None
|
|
257
|
+
if rate_limit is not None:
|
|
258
|
+
rate_limiter = build_limiter(rate_limit)
|
|
245
259
|
|
|
246
260
|
def init_openapi_2() -> SwaggerV20:
|
|
247
261
|
_maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
|
|
@@ -258,6 +272,7 @@ def from_dict(
|
|
|
258
272
|
data_generation_methods=prepare_data_generation_methods(data_generation_methods),
|
|
259
273
|
code_sample_style=_code_sample_style,
|
|
260
274
|
location=location,
|
|
275
|
+
rate_limiter=rate_limiter,
|
|
261
276
|
)
|
|
262
277
|
dispatch("after_load_schema", hook_context, instance)
|
|
263
278
|
return instance
|
|
@@ -277,6 +292,7 @@ def from_dict(
|
|
|
277
292
|
data_generation_methods=prepare_data_generation_methods(data_generation_methods),
|
|
278
293
|
code_sample_style=_code_sample_style,
|
|
279
294
|
location=location,
|
|
295
|
+
rate_limiter=rate_limiter,
|
|
280
296
|
)
|
|
281
297
|
dispatch("after_load_schema", hook_context, instance)
|
|
282
298
|
return instance
|
|
@@ -344,6 +360,7 @@ def from_pytest_fixture(
|
|
|
344
360
|
validate_schema: bool = False,
|
|
345
361
|
data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
|
|
346
362
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
363
|
+
rate_limit: Optional[str] = None,
|
|
347
364
|
) -> LazySchema:
|
|
348
365
|
"""Load schema from a ``pytest`` fixture.
|
|
349
366
|
|
|
@@ -361,6 +378,9 @@ def from_pytest_fixture(
|
|
|
361
378
|
_data_generation_methods = prepare_data_generation_methods(data_generation_methods)
|
|
362
379
|
else:
|
|
363
380
|
_data_generation_methods = data_generation_methods
|
|
381
|
+
rate_limiter: Optional[Limiter] = None
|
|
382
|
+
if rate_limit is not None:
|
|
383
|
+
rate_limiter = build_limiter(rate_limit)
|
|
364
384
|
return LazySchema(
|
|
365
385
|
fixture_name,
|
|
366
386
|
app=app,
|
|
@@ -373,6 +393,7 @@ def from_pytest_fixture(
|
|
|
373
393
|
validate_schema=validate_schema,
|
|
374
394
|
data_generation_methods=_data_generation_methods,
|
|
375
395
|
code_sample_style=_code_sample_style,
|
|
396
|
+
rate_limiter=rate_limiter,
|
|
376
397
|
)
|
|
377
398
|
|
|
378
399
|
|
|
@@ -390,6 +411,7 @@ def from_wsgi(
|
|
|
390
411
|
force_schema_version: Optional[str] = None,
|
|
391
412
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
392
413
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
414
|
+
rate_limit: Optional[str] = None,
|
|
393
415
|
**kwargs: Any,
|
|
394
416
|
) -> BaseOpenAPISchema:
|
|
395
417
|
"""Load Open API schema from a WSGI app.
|
|
@@ -416,6 +438,7 @@ def from_wsgi(
|
|
|
416
438
|
data_generation_methods=data_generation_methods,
|
|
417
439
|
code_sample_style=code_sample_style,
|
|
418
440
|
location=schema_path,
|
|
441
|
+
rate_limit=rate_limit,
|
|
419
442
|
__expects_json=_is_json_response(response),
|
|
420
443
|
)
|
|
421
444
|
|
|
@@ -442,6 +465,7 @@ def from_aiohttp(
|
|
|
442
465
|
force_schema_version: Optional[str] = None,
|
|
443
466
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
444
467
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
468
|
+
rate_limit: Optional[str] = None,
|
|
445
469
|
**kwargs: Any,
|
|
446
470
|
) -> BaseOpenAPISchema:
|
|
447
471
|
"""Load Open API schema from an AioHTTP app.
|
|
@@ -466,6 +490,7 @@ def from_aiohttp(
|
|
|
466
490
|
force_schema_version=force_schema_version,
|
|
467
491
|
data_generation_methods=data_generation_methods,
|
|
468
492
|
code_sample_style=code_sample_style,
|
|
493
|
+
rate_limit=rate_limit,
|
|
469
494
|
**kwargs,
|
|
470
495
|
)
|
|
471
496
|
|
|
@@ -484,6 +509,7 @@ def from_asgi(
|
|
|
484
509
|
force_schema_version: Optional[str] = None,
|
|
485
510
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
486
511
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
512
|
+
rate_limit: Optional[str] = None,
|
|
487
513
|
**kwargs: Any,
|
|
488
514
|
) -> BaseOpenAPISchema:
|
|
489
515
|
"""Load Open API schema from an ASGI app.
|
|
@@ -510,5 +536,6 @@ def from_asgi(
|
|
|
510
536
|
data_generation_methods=data_generation_methods,
|
|
511
537
|
code_sample_style=code_sample_style,
|
|
512
538
|
location=schema_path,
|
|
539
|
+
rate_limit=rate_limit,
|
|
513
540
|
__expects_json=_is_json_response(response),
|
|
514
541
|
)
|
|
@@ -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
|
|