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
schemathesis/runner/events.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import time
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
3
4
|
from typing import Any, Dict, List, Optional, Union
|
|
4
5
|
|
|
5
|
-
import attr
|
|
6
6
|
from requests import exceptions
|
|
7
7
|
|
|
8
8
|
from ..constants import USE_WAIT_FOR_SCHEMA_SUGGESTION_MESSAGE, DataGenerationMethod
|
|
@@ -13,7 +13,7 @@ from ..utils import current_datetime, format_exception
|
|
|
13
13
|
from .serialization import SerializedError, SerializedTestResult
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@
|
|
16
|
+
@dataclass
|
|
17
17
|
class ExecutionEvent:
|
|
18
18
|
"""Generic execution event."""
|
|
19
19
|
|
|
@@ -21,31 +21,31 @@ class ExecutionEvent:
|
|
|
21
21
|
is_terminal = False
|
|
22
22
|
|
|
23
23
|
def asdict(self, **kwargs: Any) -> Dict[str, Any]:
|
|
24
|
-
data =
|
|
24
|
+
data = asdict(self, **kwargs)
|
|
25
25
|
# An internal tag for simpler type identification
|
|
26
26
|
data["event_type"] = self.__class__.__name__
|
|
27
27
|
return data
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
@
|
|
30
|
+
@dataclass
|
|
31
31
|
class Initialized(ExecutionEvent):
|
|
32
32
|
"""Runner is initialized, settings are prepared, requests session is ready."""
|
|
33
33
|
|
|
34
|
-
schema: Dict[str, Any]
|
|
34
|
+
schema: Dict[str, Any]
|
|
35
35
|
# Total number of operations in the schema
|
|
36
|
-
operations_count: Optional[int]
|
|
36
|
+
operations_count: Optional[int]
|
|
37
37
|
# The place, where the API schema is located
|
|
38
|
-
location: Optional[str]
|
|
38
|
+
location: Optional[str]
|
|
39
39
|
# The base URL against which the tests are running
|
|
40
|
-
base_url: str
|
|
40
|
+
base_url: str
|
|
41
41
|
# API schema specification name
|
|
42
|
-
specification_name: str
|
|
42
|
+
specification_name: str
|
|
43
43
|
# Monotonic clock value when the test run started. Used to properly calculate run duration, since this clock
|
|
44
44
|
# can't go backwards.
|
|
45
|
-
start_time: float =
|
|
45
|
+
start_time: float = field(default_factory=time.monotonic)
|
|
46
46
|
# Datetime of the test run start
|
|
47
|
-
started_at: str =
|
|
48
|
-
thread_id: int =
|
|
47
|
+
started_at: str = field(default_factory=current_datetime)
|
|
48
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
51
|
def from_schema(
|
|
@@ -71,7 +71,7 @@ class CurrentOperationMixin:
|
|
|
71
71
|
return f"{self.method} {self.path}"
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
@
|
|
74
|
+
@dataclass
|
|
75
75
|
class BeforeExecution(CurrentOperationMixin, ExecutionEvent):
|
|
76
76
|
"""Happens before each tested API operation.
|
|
77
77
|
|
|
@@ -79,21 +79,21 @@ class BeforeExecution(CurrentOperationMixin, ExecutionEvent):
|
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
81
|
# HTTP method
|
|
82
|
-
method: str
|
|
82
|
+
method: str
|
|
83
83
|
# Full path, including the base path
|
|
84
|
-
path: str
|
|
84
|
+
path: str
|
|
85
85
|
# Specification-specific operation name
|
|
86
|
-
verbose_name: str
|
|
86
|
+
verbose_name: str
|
|
87
87
|
# Path without the base path
|
|
88
|
-
relative_path: str
|
|
88
|
+
relative_path: str
|
|
89
89
|
# The current level of recursion during stateful testing
|
|
90
|
-
recursion_level: int
|
|
90
|
+
recursion_level: int
|
|
91
91
|
# The way data will be generated
|
|
92
|
-
data_generation_method: List[DataGenerationMethod]
|
|
92
|
+
data_generation_method: List[DataGenerationMethod]
|
|
93
93
|
# A unique ID which connects events that happen during testing of the same API operation
|
|
94
94
|
# It may be useful when multiple threads are involved where incoming events are not ordered
|
|
95
|
-
correlation_id: str
|
|
96
|
-
thread_id: int =
|
|
95
|
+
correlation_id: str
|
|
96
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
97
97
|
|
|
98
98
|
@classmethod
|
|
99
99
|
def from_operation(
|
|
@@ -114,27 +114,27 @@ class BeforeExecution(CurrentOperationMixin, ExecutionEvent):
|
|
|
114
114
|
)
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
@
|
|
117
|
+
@dataclass
|
|
118
118
|
class AfterExecution(CurrentOperationMixin, ExecutionEvent):
|
|
119
119
|
"""Happens after each tested API operation."""
|
|
120
120
|
|
|
121
|
-
method: str
|
|
122
|
-
path: str
|
|
123
|
-
relative_path: str
|
|
121
|
+
method: str
|
|
122
|
+
path: str
|
|
123
|
+
relative_path: str
|
|
124
124
|
# Specification-specific operation name
|
|
125
|
-
verbose_name: str
|
|
125
|
+
verbose_name: str
|
|
126
126
|
|
|
127
127
|
# APIOperation test status - success / failure / error
|
|
128
|
-
status: Status
|
|
128
|
+
status: Status
|
|
129
129
|
# The way data was generated
|
|
130
|
-
data_generation_method: List[DataGenerationMethod]
|
|
131
|
-
result: SerializedTestResult
|
|
130
|
+
data_generation_method: List[DataGenerationMethod]
|
|
131
|
+
result: SerializedTestResult
|
|
132
132
|
# Test running time
|
|
133
|
-
elapsed_time: float
|
|
134
|
-
correlation_id: str
|
|
135
|
-
thread_id: int =
|
|
133
|
+
elapsed_time: float
|
|
134
|
+
correlation_id: str
|
|
135
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
136
136
|
# Captured hypothesis stdout
|
|
137
|
-
hypothesis_output: List[str] =
|
|
137
|
+
hypothesis_output: List[str] = field(default_factory=list)
|
|
138
138
|
|
|
139
139
|
@classmethod
|
|
140
140
|
def from_result(
|
|
@@ -161,24 +161,24 @@ class AfterExecution(CurrentOperationMixin, ExecutionEvent):
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
@
|
|
164
|
+
@dataclass
|
|
165
165
|
class Interrupted(ExecutionEvent):
|
|
166
166
|
"""If execution was interrupted by Ctrl-C, or a received SIGTERM."""
|
|
167
167
|
|
|
168
|
-
thread_id: int =
|
|
168
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
@
|
|
171
|
+
@dataclass
|
|
172
172
|
class InternalError(ExecutionEvent):
|
|
173
173
|
"""An error that happened inside the runner."""
|
|
174
174
|
|
|
175
175
|
is_terminal = True
|
|
176
176
|
|
|
177
|
-
message: str
|
|
178
|
-
exception_type: str
|
|
179
|
-
exception: Optional[str] =
|
|
180
|
-
exception_with_traceback: Optional[str] =
|
|
181
|
-
thread_id: int =
|
|
177
|
+
message: str
|
|
178
|
+
exception_type: str
|
|
179
|
+
exception: Optional[str] = None
|
|
180
|
+
exception_with_traceback: Optional[str] = None
|
|
181
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
182
182
|
|
|
183
183
|
@classmethod
|
|
184
184
|
def from_exc(cls, exc: Exception, wait_for_schema: Optional[float] = None) -> "InternalError":
|
|
@@ -205,7 +205,7 @@ class InternalError(ExecutionEvent):
|
|
|
205
205
|
)
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
@
|
|
208
|
+
@dataclass
|
|
209
209
|
class Finished(ExecutionEvent):
|
|
210
210
|
"""The final event of the run.
|
|
211
211
|
|
|
@@ -214,23 +214,23 @@ class Finished(ExecutionEvent):
|
|
|
214
214
|
|
|
215
215
|
is_terminal = True
|
|
216
216
|
|
|
217
|
-
passed_count: int
|
|
218
|
-
skipped_count: int
|
|
219
|
-
failed_count: int
|
|
220
|
-
errored_count: int
|
|
217
|
+
passed_count: int
|
|
218
|
+
skipped_count: int
|
|
219
|
+
failed_count: int
|
|
220
|
+
errored_count: int
|
|
221
221
|
|
|
222
|
-
has_failures: bool
|
|
223
|
-
has_errors: bool
|
|
224
|
-
has_logs: bool
|
|
225
|
-
is_empty: bool
|
|
226
|
-
generic_errors: List[SerializedError]
|
|
227
|
-
warnings: List[str]
|
|
222
|
+
has_failures: bool
|
|
223
|
+
has_errors: bool
|
|
224
|
+
has_logs: bool
|
|
225
|
+
is_empty: bool
|
|
226
|
+
generic_errors: List[SerializedError]
|
|
227
|
+
warnings: List[str]
|
|
228
228
|
|
|
229
|
-
total: Dict[str, Dict[Union[str, Status], int]]
|
|
229
|
+
total: Dict[str, Dict[Union[str, Status], int]]
|
|
230
230
|
|
|
231
231
|
# Total test run execution time
|
|
232
|
-
running_time: float
|
|
233
|
-
thread_id: int =
|
|
232
|
+
running_time: float
|
|
233
|
+
thread_id: int = field(default_factory=threading.get_ident)
|
|
234
234
|
|
|
235
235
|
@classmethod
|
|
236
236
|
def from_results(cls, results: TestResultSet, running_time: float) -> "Finished":
|
schemathesis/runner/impl/core.py
CHANGED
|
@@ -4,11 +4,11 @@ import time
|
|
|
4
4
|
import unittest
|
|
5
5
|
import uuid
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass, field
|
|
7
8
|
from types import TracebackType
|
|
8
9
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Type, Union, cast
|
|
9
10
|
from warnings import WarningMessage, catch_warnings
|
|
10
11
|
|
|
11
|
-
import attr
|
|
12
12
|
import hypothesis
|
|
13
13
|
import requests
|
|
14
14
|
from _pytest.logging import LogCaptureHandler, catching_logs
|
|
@@ -58,27 +58,27 @@ def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
|
|
58
58
|
return isinstance(event, events.AfterExecution) and event.status in (Status.error, Status.failure)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
@
|
|
61
|
+
@dataclass
|
|
62
62
|
class BaseRunner:
|
|
63
|
-
schema: BaseSchema
|
|
64
|
-
checks: Iterable[CheckFunction]
|
|
65
|
-
max_response_time: Optional[int]
|
|
66
|
-
targets: Iterable[Target]
|
|
67
|
-
hypothesis_settings: hypothesis.settings
|
|
68
|
-
auth: Optional[RawAuth] =
|
|
69
|
-
auth_type: Optional[str] =
|
|
70
|
-
headers: Optional[Dict[str, Any]] =
|
|
71
|
-
request_timeout: Optional[int] =
|
|
72
|
-
store_interactions: bool =
|
|
73
|
-
seed: Optional[int] =
|
|
74
|
-
exit_first: bool =
|
|
75
|
-
max_failures: Optional[int] =
|
|
76
|
-
started_at: str =
|
|
77
|
-
dry_run: bool =
|
|
78
|
-
stateful: Optional[Stateful] =
|
|
79
|
-
stateful_recursion_limit: int =
|
|
80
|
-
count_operations: bool =
|
|
81
|
-
_failures_counter: int =
|
|
63
|
+
schema: BaseSchema
|
|
64
|
+
checks: Iterable[CheckFunction]
|
|
65
|
+
max_response_time: Optional[int]
|
|
66
|
+
targets: Iterable[Target]
|
|
67
|
+
hypothesis_settings: hypothesis.settings
|
|
68
|
+
auth: Optional[RawAuth] = None
|
|
69
|
+
auth_type: Optional[str] = None
|
|
70
|
+
headers: Optional[Dict[str, Any]] = None
|
|
71
|
+
request_timeout: Optional[int] = None
|
|
72
|
+
store_interactions: bool = False
|
|
73
|
+
seed: Optional[int] = None
|
|
74
|
+
exit_first: bool = False
|
|
75
|
+
max_failures: Optional[int] = None
|
|
76
|
+
started_at: str = field(default_factory=current_datetime)
|
|
77
|
+
dry_run: bool = False
|
|
78
|
+
stateful: Optional[Stateful] = None
|
|
79
|
+
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
|
|
80
|
+
count_operations: bool = True
|
|
81
|
+
_failures_counter: int = 0
|
|
82
82
|
|
|
83
83
|
def execute(self) -> "EventStream":
|
|
84
84
|
"""Common logic for all runners."""
|
|
@@ -199,15 +199,15 @@ class BaseRunner:
|
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
@
|
|
202
|
+
@dataclass
|
|
203
203
|
class EventStream:
|
|
204
204
|
"""Schemathesis event stream.
|
|
205
205
|
|
|
206
206
|
Provides an API to control the execution flow.
|
|
207
207
|
"""
|
|
208
208
|
|
|
209
|
-
generator: Generator[events.ExecutionEvent, None, None]
|
|
210
|
-
stop_event: threading.Event
|
|
209
|
+
generator: Generator[events.ExecutionEvent, None, None]
|
|
210
|
+
stop_event: threading.Event
|
|
211
211
|
|
|
212
212
|
def __next__(self) -> events.ExecutionEvent:
|
|
213
213
|
return next(self.generator)
|
|
@@ -553,7 +553,7 @@ def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any)
|
|
|
553
553
|
test(_case, *args)
|
|
554
554
|
|
|
555
555
|
|
|
556
|
-
@
|
|
556
|
+
@dataclass
|
|
557
557
|
class ErrorCollector:
|
|
558
558
|
"""Collect exceptions that are not related to failed checks.
|
|
559
559
|
|
|
@@ -566,7 +566,7 @@ class ErrorCollector:
|
|
|
566
566
|
function signatures, which are used by Hypothesis.
|
|
567
567
|
"""
|
|
568
568
|
|
|
569
|
-
errors: List[Exception]
|
|
569
|
+
errors: List[Exception]
|
|
570
570
|
|
|
571
571
|
def __enter__(self) -> "ErrorCollector":
|
|
572
572
|
return self
|
schemathesis/runner/impl/solo.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import threading
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from typing import Generator, Optional, Union
|
|
3
4
|
|
|
4
|
-
import attr
|
|
5
|
-
|
|
6
5
|
from ...models import TestResultSet
|
|
7
6
|
from ...types import RequestCert
|
|
8
7
|
from ...utils import get_requests_auth
|
|
@@ -10,12 +9,12 @@ from .. import events
|
|
|
10
9
|
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
@
|
|
12
|
+
@dataclass
|
|
14
13
|
class SingleThreadRunner(BaseRunner):
|
|
15
14
|
"""Fast runner that runs tests sequentially in the main thread."""
|
|
16
15
|
|
|
17
|
-
request_tls_verify: Union[bool, str] =
|
|
18
|
-
request_cert: Optional[RequestCert] =
|
|
16
|
+
request_tls_verify: Union[bool, str] = True
|
|
17
|
+
request_cert: Optional[RequestCert] = None
|
|
19
18
|
|
|
20
19
|
def _execute(
|
|
21
20
|
self, results: TestResultSet, stop_event: threading.Event
|
|
@@ -47,7 +46,7 @@ class SingleThreadRunner(BaseRunner):
|
|
|
47
46
|
)
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
@
|
|
49
|
+
@dataclass
|
|
51
50
|
class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
52
51
|
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
53
52
|
yield from self._run_tests(
|
|
@@ -67,7 +66,7 @@ class SingleThreadWSGIRunner(SingleThreadRunner):
|
|
|
67
66
|
)
|
|
68
67
|
|
|
69
68
|
|
|
70
|
-
@
|
|
69
|
+
@dataclass
|
|
71
70
|
class SingleThreadASGIRunner(SingleThreadRunner):
|
|
72
71
|
def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
73
72
|
yield from self._run_tests(
|
|
@@ -2,10 +2,10 @@ import ctypes
|
|
|
2
2
|
import queue
|
|
3
3
|
import threading
|
|
4
4
|
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from queue import Queue
|
|
6
7
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Union, cast
|
|
7
8
|
|
|
8
|
-
import attr
|
|
9
9
|
import hypothesis
|
|
10
10
|
|
|
11
11
|
from ..._hypothesis import create_test
|
|
@@ -201,13 +201,13 @@ def stop_worker(thread_id: int) -> None:
|
|
|
201
201
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
@
|
|
204
|
+
@dataclass
|
|
205
205
|
class ThreadPoolRunner(BaseRunner):
|
|
206
206
|
"""Spread different tests among multiple worker threads."""
|
|
207
207
|
|
|
208
|
-
workers_num: int =
|
|
209
|
-
request_tls_verify: Union[bool, str] =
|
|
210
|
-
request_cert: Optional[RequestCert] =
|
|
208
|
+
workers_num: int = 2
|
|
209
|
+
request_tls_verify: Union[bool, str] = True
|
|
210
|
+
request_cert: Optional[RequestCert] = None
|
|
211
211
|
|
|
212
212
|
def _execute(
|
|
213
213
|
self, results: TestResultSet, stop_event: threading.Event
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
They all consist of primitive types and don't have references to schemas, app, etc.
|
|
4
4
|
"""
|
|
5
5
|
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
6
7
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
7
8
|
|
|
8
|
-
import attr
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
11
|
from ..exceptions import FailureContext, InternalError, make_unique_by_key
|
|
@@ -13,17 +13,17 @@ from ..models import Case, Check, Interaction, Request, Response, Status, TestRe
|
|
|
13
13
|
from ..utils import IGNORED_HEADERS, WSGIResponse, format_exception
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
@
|
|
16
|
+
@dataclass
|
|
17
17
|
class SerializedCase:
|
|
18
|
-
requests_code: str
|
|
19
|
-
curl_code: str
|
|
20
|
-
path_template: str
|
|
21
|
-
path_parameters: Optional[Dict[str, Any]]
|
|
22
|
-
query: Optional[Dict[str, Any]]
|
|
23
|
-
cookies: Optional[Dict[str, Any]]
|
|
24
|
-
verbose_name: str
|
|
25
|
-
data_generation_method: Optional[str]
|
|
26
|
-
media_type: Optional[str]
|
|
18
|
+
requests_code: str
|
|
19
|
+
curl_code: str
|
|
20
|
+
path_template: str
|
|
21
|
+
path_parameters: Optional[Dict[str, Any]]
|
|
22
|
+
query: Optional[Dict[str, Any]]
|
|
23
|
+
cookies: Optional[Dict[str, Any]]
|
|
24
|
+
verbose_name: str
|
|
25
|
+
data_generation_method: Optional[str]
|
|
26
|
+
media_type: Optional[str]
|
|
27
27
|
|
|
28
28
|
@classmethod
|
|
29
29
|
def from_case(cls, case: Case, headers: Optional[Dict[str, Any]]) -> "SerializedCase":
|
|
@@ -42,21 +42,21 @@ class SerializedCase:
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
@
|
|
45
|
+
@dataclass
|
|
46
46
|
class SerializedCheck:
|
|
47
47
|
# Check name
|
|
48
|
-
name: str
|
|
48
|
+
name: str
|
|
49
49
|
# Check result
|
|
50
|
-
value: Status
|
|
51
|
-
request: Request
|
|
52
|
-
response: Optional[Response]
|
|
50
|
+
value: Status
|
|
51
|
+
request: Request
|
|
52
|
+
response: Optional[Response]
|
|
53
53
|
# Generated example
|
|
54
|
-
example: SerializedCase
|
|
55
|
-
message: Optional[str] =
|
|
54
|
+
example: SerializedCase
|
|
55
|
+
message: Optional[str] = None
|
|
56
56
|
# Failure-specific context
|
|
57
|
-
context: Optional[FailureContext] =
|
|
57
|
+
context: Optional[FailureContext] = None
|
|
58
58
|
# Cases & responses that were made before this one
|
|
59
|
-
history: List["SerializedHistoryEntry"] =
|
|
59
|
+
history: List["SerializedHistoryEntry"] = field(default_factory=list)
|
|
60
60
|
|
|
61
61
|
@classmethod
|
|
62
62
|
def from_check(cls, check: Check) -> "SerializedCheck":
|
|
@@ -100,18 +100,18 @@ class SerializedCheck:
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
@
|
|
103
|
+
@dataclass
|
|
104
104
|
class SerializedHistoryEntry:
|
|
105
|
-
case: SerializedCase
|
|
106
|
-
response: Response
|
|
105
|
+
case: SerializedCase
|
|
106
|
+
response: Response
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
@
|
|
109
|
+
@dataclass
|
|
110
110
|
class SerializedError:
|
|
111
|
-
exception: str
|
|
112
|
-
exception_with_traceback: str
|
|
113
|
-
example: Optional[SerializedCase]
|
|
114
|
-
title: Optional[str]
|
|
111
|
+
exception: str
|
|
112
|
+
exception_with_traceback: str
|
|
113
|
+
example: Optional[SerializedCase]
|
|
114
|
+
title: Optional[str]
|
|
115
115
|
|
|
116
116
|
@classmethod
|
|
117
117
|
def from_error(
|
|
@@ -125,13 +125,13 @@ class SerializedError:
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
@
|
|
128
|
+
@dataclass
|
|
129
129
|
class SerializedInteraction:
|
|
130
|
-
request: Request
|
|
131
|
-
response: Response
|
|
132
|
-
checks: List[SerializedCheck]
|
|
133
|
-
status: Status
|
|
134
|
-
recorded_at: str
|
|
130
|
+
request: Request
|
|
131
|
+
response: Response
|
|
132
|
+
checks: List[SerializedCheck]
|
|
133
|
+
status: Status
|
|
134
|
+
recorded_at: str
|
|
135
135
|
|
|
136
136
|
@classmethod
|
|
137
137
|
def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
|
|
@@ -144,23 +144,23 @@ class SerializedInteraction:
|
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
@
|
|
147
|
+
@dataclass
|
|
148
148
|
class SerializedTestResult:
|
|
149
|
-
method: str
|
|
150
|
-
path: str
|
|
151
|
-
verbose_name: str
|
|
152
|
-
has_failures: bool
|
|
153
|
-
has_errors: bool
|
|
154
|
-
has_logs: bool
|
|
155
|
-
is_errored: bool
|
|
156
|
-
is_flaky: bool
|
|
157
|
-
is_skipped: bool
|
|
158
|
-
seed: Optional[int]
|
|
159
|
-
data_generation_method: List[str]
|
|
160
|
-
checks: List[SerializedCheck]
|
|
161
|
-
logs: List[str]
|
|
162
|
-
errors: List[SerializedError]
|
|
163
|
-
interactions: List[SerializedInteraction]
|
|
149
|
+
method: str
|
|
150
|
+
path: str
|
|
151
|
+
verbose_name: str
|
|
152
|
+
has_failures: bool
|
|
153
|
+
has_errors: bool
|
|
154
|
+
has_logs: bool
|
|
155
|
+
is_errored: bool
|
|
156
|
+
is_flaky: bool
|
|
157
|
+
is_skipped: bool
|
|
158
|
+
seed: Optional[int]
|
|
159
|
+
data_generation_method: List[str]
|
|
160
|
+
checks: List[SerializedCheck]
|
|
161
|
+
logs: List[str]
|
|
162
|
+
errors: List[SerializedError]
|
|
163
|
+
interactions: List[SerializedInteraction]
|
|
164
164
|
|
|
165
165
|
@classmethod
|
|
166
166
|
def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
|
schemathesis/schemas.py
CHANGED
|
@@ -7,11 +7,14 @@ Their responsibilities:
|
|
|
7
7
|
They give only static definitions of paths.
|
|
8
8
|
"""
|
|
9
9
|
from collections.abc import Mapping
|
|
10
|
+
from contextlib import nullcontext
|
|
11
|
+
from dataclasses import dataclass, field
|
|
10
12
|
from difflib import get_close_matches
|
|
11
13
|
from functools import lru_cache
|
|
12
14
|
from typing import (
|
|
13
15
|
Any,
|
|
14
16
|
Callable,
|
|
17
|
+
ContextManager,
|
|
15
18
|
Dict,
|
|
16
19
|
Generator,
|
|
17
20
|
Iterable,
|
|
@@ -25,11 +28,11 @@ from typing import (
|
|
|
25
28
|
TypeVar,
|
|
26
29
|
Union,
|
|
27
30
|
)
|
|
28
|
-
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
|
31
|
+
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
29
32
|
|
|
30
|
-
import attr
|
|
31
33
|
import hypothesis
|
|
32
34
|
from hypothesis.strategies import SearchStrategy
|
|
35
|
+
from pyrate_limiter import Limiter
|
|
33
36
|
from requests.structures import CaseInsensitiveDict
|
|
34
37
|
|
|
35
38
|
from ._hypothesis import create_test
|
|
@@ -74,28 +77,29 @@ C = TypeVar("C", bound=Case)
|
|
|
74
77
|
|
|
75
78
|
@lru_cache()
|
|
76
79
|
def get_full_path(base_path: str, path: str) -> str:
|
|
77
|
-
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
|
80
|
+
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
@
|
|
83
|
+
@dataclass(eq=False)
|
|
81
84
|
class BaseSchema(Mapping):
|
|
82
|
-
raw_schema: Dict[str, Any]
|
|
83
|
-
location: Optional[str] =
|
|
84
|
-
base_url: Optional[str] =
|
|
85
|
-
method: Optional[Filter] =
|
|
86
|
-
endpoint: Optional[Filter] =
|
|
87
|
-
tag: Optional[Filter] =
|
|
88
|
-
operation_id: Optional[Filter] =
|
|
89
|
-
app: Any =
|
|
90
|
-
hooks: HookDispatcher =
|
|
91
|
-
auth: AuthStorage =
|
|
92
|
-
test_function: Optional[GenericTest] =
|
|
93
|
-
validate_schema: bool =
|
|
94
|
-
skip_deprecated_operations: bool =
|
|
95
|
-
data_generation_methods: List[DataGenerationMethod] =
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
code_sample_style: CodeSampleStyle =
|
|
85
|
+
raw_schema: Dict[str, Any]
|
|
86
|
+
location: Optional[str] = None
|
|
87
|
+
base_url: Optional[str] = None
|
|
88
|
+
method: Optional[Filter] = None
|
|
89
|
+
endpoint: Optional[Filter] = None
|
|
90
|
+
tag: Optional[Filter] = None
|
|
91
|
+
operation_id: Optional[Filter] = None
|
|
92
|
+
app: Any = None
|
|
93
|
+
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
|
94
|
+
auth: AuthStorage = field(default_factory=AuthStorage)
|
|
95
|
+
test_function: Optional[GenericTest] = None
|
|
96
|
+
validate_schema: bool = True
|
|
97
|
+
skip_deprecated_operations: bool = False
|
|
98
|
+
data_generation_methods: List[DataGenerationMethod] = field(
|
|
99
|
+
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
|
100
|
+
)
|
|
101
|
+
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
102
|
+
rate_limiter: Optional[Limiter] = None
|
|
99
103
|
|
|
100
104
|
def __iter__(self) -> Iterator[str]:
|
|
101
105
|
return iter(self.operations)
|
|
@@ -116,7 +120,7 @@ class BaseSchema(Mapping):
|
|
|
116
120
|
def hook(self, hook: Union[str, Callable]) -> Callable:
|
|
117
121
|
return self.hooks.register(hook)
|
|
118
122
|
|
|
119
|
-
@property
|
|
123
|
+
@property
|
|
120
124
|
def verbose_name(self) -> str:
|
|
121
125
|
raise NotImplementedError
|
|
122
126
|
|
|
@@ -148,7 +152,7 @@ class BaseSchema(Mapping):
|
|
|
148
152
|
def get_base_url(self) -> str:
|
|
149
153
|
base_url = self.base_url
|
|
150
154
|
if base_url is not None:
|
|
151
|
-
return base_url.rstrip("/")
|
|
155
|
+
return base_url.rstrip("/")
|
|
152
156
|
return self._build_base_url()
|
|
153
157
|
|
|
154
158
|
@property
|
|
@@ -271,6 +275,7 @@ class BaseSchema(Mapping):
|
|
|
271
275
|
skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
|
|
272
276
|
data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
|
|
273
277
|
code_sample_style: Union[CodeSampleStyle, NotSet] = NOT_SET,
|
|
278
|
+
rate_limiter: Optional[Limiter] = NOT_SET,
|
|
274
279
|
) -> "BaseSchema":
|
|
275
280
|
if base_url is NOT_SET:
|
|
276
281
|
base_url = self.base_url
|
|
@@ -296,6 +301,8 @@ class BaseSchema(Mapping):
|
|
|
296
301
|
data_generation_methods = self.data_generation_methods
|
|
297
302
|
if code_sample_style is NOT_SET:
|
|
298
303
|
code_sample_style = self.code_sample_style
|
|
304
|
+
if rate_limiter is NOT_SET:
|
|
305
|
+
rate_limiter = self.rate_limiter
|
|
299
306
|
|
|
300
307
|
return self.__class__(
|
|
301
308
|
self.raw_schema,
|
|
@@ -313,6 +320,7 @@ class BaseSchema(Mapping):
|
|
|
313
320
|
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
|
314
321
|
data_generation_methods=data_generation_methods, # type: ignore
|
|
315
322
|
code_sample_style=code_sample_style, # type: ignore
|
|
323
|
+
rate_limiter=rate_limiter, # type: ignore
|
|
316
324
|
)
|
|
317
325
|
|
|
318
326
|
def get_local_hook_dispatcher(self) -> Optional[HookDispatcher]:
|
|
@@ -383,6 +391,13 @@ class BaseSchema(Mapping):
|
|
|
383
391
|
def prepare_schema(self, schema: Any) -> Any:
|
|
384
392
|
raise NotImplementedError
|
|
385
393
|
|
|
394
|
+
def ratelimit(self) -> ContextManager:
|
|
395
|
+
"""Limit the rate of sending generated requests."""
|
|
396
|
+
label = urlparse(self.base_url).netloc
|
|
397
|
+
if self.rate_limiter is not None:
|
|
398
|
+
return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
|
|
399
|
+
return nullcontext()
|
|
400
|
+
|
|
386
401
|
|
|
387
402
|
def operations_to_dict(
|
|
388
403
|
operations: Generator[Result[APIOperation, InvalidSchema], None, None]
|