schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
"""Schema objects provide a convenient interface to raw schemas.
|
|
2
|
-
|
|
3
|
-
Their responsibilities:
|
|
4
|
-
- Provide a unified way to work with different types of schemas
|
|
5
|
-
- Give all paths / methods combinations that are available directly from the schema;
|
|
6
|
-
|
|
7
|
-
They give only static definitions of paths.
|
|
8
|
-
"""
|
|
9
1
|
from __future__ import annotations
|
|
10
|
-
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
11
4
|
from contextlib import nullcontext
|
|
12
5
|
from dataclasses import dataclass, field
|
|
13
6
|
from functools import lru_cache
|
|
14
7
|
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
15
9
|
Any,
|
|
16
10
|
Callable,
|
|
17
11
|
ContextManager,
|
|
@@ -21,45 +15,58 @@ from typing import (
|
|
|
21
15
|
NoReturn,
|
|
22
16
|
Sequence,
|
|
23
17
|
TypeVar,
|
|
24
|
-
TYPE_CHECKING,
|
|
25
18
|
)
|
|
26
19
|
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
27
20
|
|
|
28
|
-
import
|
|
29
|
-
from hypothesis.strategies import SearchStrategy
|
|
30
|
-
from pyrate_limiter import Limiter
|
|
31
|
-
|
|
32
|
-
from .constants import NOT_SET
|
|
21
|
+
from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
|
|
33
22
|
from ._hypothesis import create_test
|
|
34
23
|
from .auths import AuthStorage
|
|
35
24
|
from .code_samples import CodeSampleStyle
|
|
25
|
+
from .constants import NOT_SET
|
|
26
|
+
from .exceptions import OperationSchemaError, UsageError
|
|
27
|
+
from .filters import (
|
|
28
|
+
FilterSet,
|
|
29
|
+
FilterValue,
|
|
30
|
+
MatcherFunc,
|
|
31
|
+
RegexValue,
|
|
32
|
+
filter_set_from_components,
|
|
33
|
+
is_deprecated,
|
|
34
|
+
)
|
|
36
35
|
from .generation import (
|
|
37
36
|
DEFAULT_DATA_GENERATION_METHODS,
|
|
38
37
|
DataGenerationMethod,
|
|
39
38
|
DataGenerationMethodInput,
|
|
40
39
|
GenerationConfig,
|
|
40
|
+
combine_strategies,
|
|
41
41
|
)
|
|
42
|
-
from .
|
|
43
|
-
from .
|
|
44
|
-
from .internal.
|
|
42
|
+
from .hooks import HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
|
|
43
|
+
from .internal.deprecation import warn_filtration_arguments
|
|
44
|
+
from .internal.output import OutputConfig
|
|
45
|
+
from .internal.result import Ok, Result
|
|
45
46
|
from .models import APIOperation, Case
|
|
46
|
-
from .
|
|
47
|
-
from .stateful import Stateful, StatefulTest
|
|
48
|
-
from .types import (
|
|
49
|
-
Body,
|
|
50
|
-
Cookies,
|
|
51
|
-
Filter,
|
|
52
|
-
FormData,
|
|
53
|
-
GenericTest,
|
|
54
|
-
Headers,
|
|
55
|
-
NotSet,
|
|
56
|
-
PathParameters,
|
|
57
|
-
Query,
|
|
58
|
-
)
|
|
59
|
-
from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy, combine_strategies
|
|
47
|
+
from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
|
|
60
48
|
|
|
61
49
|
if TYPE_CHECKING:
|
|
50
|
+
import hypothesis
|
|
51
|
+
from hypothesis.strategies import SearchStrategy
|
|
52
|
+
from pyrate_limiter import Limiter
|
|
53
|
+
|
|
54
|
+
from .stateful import Stateful, StatefulTest
|
|
55
|
+
from .stateful.state_machine import APIStateMachine
|
|
56
|
+
from .transports import Transport
|
|
62
57
|
from .transports.responses import GenericResponse
|
|
58
|
+
from .types import (
|
|
59
|
+
Body,
|
|
60
|
+
Cookies,
|
|
61
|
+
Filter,
|
|
62
|
+
FormData,
|
|
63
|
+
GenericTest,
|
|
64
|
+
Headers,
|
|
65
|
+
NotSet,
|
|
66
|
+
PathParameters,
|
|
67
|
+
Query,
|
|
68
|
+
Specification,
|
|
69
|
+
)
|
|
63
70
|
|
|
64
71
|
|
|
65
72
|
C = TypeVar("C", bound=Case)
|
|
@@ -73,41 +80,118 @@ def get_full_path(base_path: str, path: str) -> str:
|
|
|
73
80
|
@dataclass(eq=False)
|
|
74
81
|
class BaseSchema(Mapping):
|
|
75
82
|
raw_schema: dict[str, Any]
|
|
83
|
+
transport: Transport
|
|
84
|
+
specification: Specification
|
|
76
85
|
location: str | None = None
|
|
77
86
|
base_url: str | None = None
|
|
78
|
-
|
|
79
|
-
endpoint: Filter | None = None
|
|
80
|
-
tag: Filter | None = None
|
|
81
|
-
operation_id: Filter | None = None
|
|
87
|
+
filter_set: FilterSet = field(default_factory=FilterSet)
|
|
82
88
|
app: Any = None
|
|
83
89
|
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
|
84
90
|
auth: AuthStorage = field(default_factory=AuthStorage)
|
|
85
91
|
test_function: GenericTest | None = None
|
|
86
92
|
validate_schema: bool = True
|
|
87
|
-
skip_deprecated_operations: bool = False
|
|
88
93
|
data_generation_methods: list[DataGenerationMethod] = field(
|
|
89
94
|
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
|
90
95
|
)
|
|
91
96
|
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
|
97
|
+
output_config: OutputConfig = field(default_factory=OutputConfig)
|
|
92
98
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
93
99
|
rate_limiter: Limiter | None = None
|
|
94
100
|
sanitize_output: bool = True
|
|
95
101
|
|
|
102
|
+
def __post_init__(self) -> None:
|
|
103
|
+
self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
|
|
104
|
+
|
|
105
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
106
|
+
|
|
107
|
+
def include(
|
|
108
|
+
self,
|
|
109
|
+
func: MatcherFunc | None = None,
|
|
110
|
+
*,
|
|
111
|
+
name: FilterValue | None = None,
|
|
112
|
+
name_regex: str | None = None,
|
|
113
|
+
method: FilterValue | None = None,
|
|
114
|
+
method_regex: str | None = None,
|
|
115
|
+
path: FilterValue | None = None,
|
|
116
|
+
path_regex: str | None = None,
|
|
117
|
+
tag: FilterValue | None = None,
|
|
118
|
+
tag_regex: RegexValue | None = None,
|
|
119
|
+
operation_id: FilterValue | None = None,
|
|
120
|
+
operation_id_regex: RegexValue | None = None,
|
|
121
|
+
) -> BaseSchema:
|
|
122
|
+
"""Include only operations that match the given filters."""
|
|
123
|
+
filter_set = self.filter_set.clone()
|
|
124
|
+
filter_set.include(
|
|
125
|
+
func,
|
|
126
|
+
name=name,
|
|
127
|
+
name_regex=name_regex,
|
|
128
|
+
method=method,
|
|
129
|
+
method_regex=method_regex,
|
|
130
|
+
path=path,
|
|
131
|
+
path_regex=path_regex,
|
|
132
|
+
tag=tag,
|
|
133
|
+
tag_regex=tag_regex,
|
|
134
|
+
operation_id=operation_id,
|
|
135
|
+
operation_id_regex=operation_id_regex,
|
|
136
|
+
)
|
|
137
|
+
return self.clone(filter_set=filter_set)
|
|
138
|
+
|
|
139
|
+
def exclude(
|
|
140
|
+
self,
|
|
141
|
+
func: MatcherFunc | None = None,
|
|
142
|
+
*,
|
|
143
|
+
name: FilterValue | None = None,
|
|
144
|
+
name_regex: str | None = None,
|
|
145
|
+
method: FilterValue | None = None,
|
|
146
|
+
method_regex: str | None = None,
|
|
147
|
+
path: FilterValue | None = None,
|
|
148
|
+
path_regex: str | None = None,
|
|
149
|
+
tag: FilterValue | None = None,
|
|
150
|
+
tag_regex: RegexValue | None = None,
|
|
151
|
+
operation_id: FilterValue | None = None,
|
|
152
|
+
operation_id_regex: RegexValue | None = None,
|
|
153
|
+
deprecated: bool = False,
|
|
154
|
+
) -> BaseSchema:
|
|
155
|
+
"""Include only operations that match the given filters."""
|
|
156
|
+
filter_set = self.filter_set.clone()
|
|
157
|
+
if deprecated:
|
|
158
|
+
if func is None:
|
|
159
|
+
func = is_deprecated
|
|
160
|
+
else:
|
|
161
|
+
filter_set.exclude(is_deprecated)
|
|
162
|
+
filter_set.exclude(
|
|
163
|
+
func,
|
|
164
|
+
name=name,
|
|
165
|
+
name_regex=name_regex,
|
|
166
|
+
method=method,
|
|
167
|
+
method_regex=method_regex,
|
|
168
|
+
path=path,
|
|
169
|
+
path_regex=path_regex,
|
|
170
|
+
tag=tag,
|
|
171
|
+
tag_regex=tag_regex,
|
|
172
|
+
operation_id=operation_id,
|
|
173
|
+
operation_id_regex=operation_id_regex,
|
|
174
|
+
)
|
|
175
|
+
return self.clone(filter_set=filter_set)
|
|
176
|
+
|
|
96
177
|
def __iter__(self) -> Iterator[str]:
|
|
97
|
-
|
|
178
|
+
raise NotImplementedError
|
|
98
179
|
|
|
99
180
|
def __getitem__(self, item: str) -> APIOperationMap:
|
|
100
181
|
__tracebackhide__ = True
|
|
101
182
|
try:
|
|
102
|
-
return self.
|
|
183
|
+
return self._get_operation_map(item)
|
|
103
184
|
except KeyError as exc:
|
|
104
185
|
self.on_missing_operation(item, exc)
|
|
105
186
|
|
|
187
|
+
def _get_operation_map(self, key: str) -> APIOperationMap:
|
|
188
|
+
raise NotImplementedError
|
|
189
|
+
|
|
106
190
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
|
107
191
|
raise NotImplementedError
|
|
108
192
|
|
|
109
193
|
def __len__(self) -> int:
|
|
110
|
-
return
|
|
194
|
+
return self.operations_count
|
|
111
195
|
|
|
112
196
|
def hook(self, hook: str | Callable) -> Callable:
|
|
113
197
|
return self.hooks.register(hook)
|
|
@@ -150,18 +234,6 @@ class BaseSchema(Mapping):
|
|
|
150
234
|
def validate(self) -> None:
|
|
151
235
|
raise NotImplementedError
|
|
152
236
|
|
|
153
|
-
@property
|
|
154
|
-
def operations(self) -> dict[str, APIOperationMap]:
|
|
155
|
-
if not hasattr(self, "_operations"):
|
|
156
|
-
operations = self.get_all_operations()
|
|
157
|
-
self._operations = self._store_operations(operations)
|
|
158
|
-
return self._operations
|
|
159
|
-
|
|
160
|
-
def _store_operations(
|
|
161
|
-
self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
|
|
162
|
-
) -> dict[str, APIOperationMap]:
|
|
163
|
-
raise NotImplementedError
|
|
164
|
-
|
|
165
237
|
@property
|
|
166
238
|
def operations_count(self) -> int:
|
|
167
239
|
raise NotImplementedError
|
|
@@ -171,11 +243,13 @@ class BaseSchema(Mapping):
|
|
|
171
243
|
raise NotImplementedError
|
|
172
244
|
|
|
173
245
|
def get_all_operations(
|
|
174
|
-
self, hooks: HookDispatcher | None = None
|
|
246
|
+
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
|
175
247
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
176
248
|
raise NotImplementedError
|
|
177
249
|
|
|
178
|
-
def get_strategies_from_examples(
|
|
250
|
+
def get_strategies_from_examples(
|
|
251
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
252
|
+
) -> list[SearchStrategy[Case]]:
|
|
179
253
|
"""Get examples from the API operation."""
|
|
180
254
|
raise NotImplementedError
|
|
181
255
|
|
|
@@ -204,7 +278,7 @@ class BaseSchema(Mapping):
|
|
|
204
278
|
_given_kwargs: dict[str, GivenInput] | None = None,
|
|
205
279
|
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
|
206
280
|
"""Generate all operations and Hypothesis tests for them."""
|
|
207
|
-
for result in self.get_all_operations(hooks=hooks):
|
|
281
|
+
for result in self.get_all_operations(hooks=hooks, generation_config=generation_config):
|
|
208
282
|
if isinstance(result, Ok):
|
|
209
283
|
operation = result.ok()
|
|
210
284
|
_as_strategy_kwargs: dict[str, Any] | None
|
|
@@ -242,6 +316,21 @@ class BaseSchema(Mapping):
|
|
|
242
316
|
CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
|
|
243
317
|
)
|
|
244
318
|
|
|
319
|
+
for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
|
|
320
|
+
value = locals()[name]
|
|
321
|
+
if value is not NOT_SET:
|
|
322
|
+
warn_filtration_arguments(name)
|
|
323
|
+
|
|
324
|
+
filter_set = filter_set_from_components(
|
|
325
|
+
include=True,
|
|
326
|
+
method=method,
|
|
327
|
+
endpoint=endpoint,
|
|
328
|
+
tag=tag,
|
|
329
|
+
operation_id=operation_id,
|
|
330
|
+
skip_deprecated_operations=skip_deprecated_operations,
|
|
331
|
+
parent=self.filter_set,
|
|
332
|
+
)
|
|
333
|
+
|
|
245
334
|
def wrapper(func: GenericTest) -> GenericTest:
|
|
246
335
|
if hasattr(func, PARAMETRIZE_MARKER):
|
|
247
336
|
|
|
@@ -256,13 +345,9 @@ class BaseSchema(Mapping):
|
|
|
256
345
|
HookDispatcher.add_dispatcher(func)
|
|
257
346
|
cloned = self.clone(
|
|
258
347
|
test_function=func,
|
|
259
|
-
method=method,
|
|
260
|
-
endpoint=endpoint,
|
|
261
|
-
tag=tag,
|
|
262
|
-
operation_id=operation_id,
|
|
263
348
|
validate_schema=validate_schema,
|
|
264
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
265
349
|
data_generation_methods=data_generation_methods,
|
|
350
|
+
filter_set=filter_set,
|
|
266
351
|
code_sample_style=_code_sample_style, # type: ignore
|
|
267
352
|
)
|
|
268
353
|
setattr(func, PARAMETRIZE_MARKER, cloned)
|
|
@@ -279,37 +364,26 @@ class BaseSchema(Mapping):
|
|
|
279
364
|
*,
|
|
280
365
|
base_url: str | None | NotSet = NOT_SET,
|
|
281
366
|
test_function: GenericTest | None = None,
|
|
282
|
-
method: Filter | None = NOT_SET,
|
|
283
|
-
endpoint: Filter | None = NOT_SET,
|
|
284
|
-
tag: Filter | None = NOT_SET,
|
|
285
|
-
operation_id: Filter | None = NOT_SET,
|
|
286
367
|
app: Any = NOT_SET,
|
|
287
368
|
hooks: HookDispatcher | NotSet = NOT_SET,
|
|
288
369
|
auth: AuthStorage | NotSet = NOT_SET,
|
|
289
370
|
validate_schema: bool | NotSet = NOT_SET,
|
|
290
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
291
371
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
292
372
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
373
|
+
output_config: OutputConfig | NotSet = NOT_SET,
|
|
293
374
|
code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
|
|
294
375
|
rate_limiter: Limiter | None = NOT_SET,
|
|
295
376
|
sanitize_output: bool | NotSet | None = NOT_SET,
|
|
377
|
+
filter_set: FilterSet | None = None,
|
|
296
378
|
) -> BaseSchema:
|
|
297
379
|
if base_url is NOT_SET:
|
|
298
380
|
base_url = self.base_url
|
|
299
|
-
if method is NOT_SET:
|
|
300
|
-
method = self.method
|
|
301
|
-
if endpoint is NOT_SET:
|
|
302
|
-
endpoint = self.endpoint
|
|
303
|
-
if tag is NOT_SET:
|
|
304
|
-
tag = self.tag
|
|
305
|
-
if operation_id is NOT_SET:
|
|
306
|
-
operation_id = self.operation_id
|
|
307
381
|
if app is NOT_SET:
|
|
308
382
|
app = self.app
|
|
309
383
|
if validate_schema is NOT_SET:
|
|
310
384
|
validate_schema = self.validate_schema
|
|
311
|
-
if
|
|
312
|
-
|
|
385
|
+
if filter_set is None:
|
|
386
|
+
filter_set = self.filter_set
|
|
313
387
|
if hooks is NOT_SET:
|
|
314
388
|
hooks = self.hooks
|
|
315
389
|
if auth is NOT_SET:
|
|
@@ -318,6 +392,8 @@ class BaseSchema(Mapping):
|
|
|
318
392
|
data_generation_methods = self.data_generation_methods
|
|
319
393
|
if generation_config is NOT_SET:
|
|
320
394
|
generation_config = self.generation_config
|
|
395
|
+
if output_config is NOT_SET:
|
|
396
|
+
output_config = self.output_config
|
|
321
397
|
if code_sample_style is NOT_SET:
|
|
322
398
|
code_sample_style = self.code_sample_style
|
|
323
399
|
if rate_limiter is NOT_SET:
|
|
@@ -327,23 +403,22 @@ class BaseSchema(Mapping):
|
|
|
327
403
|
|
|
328
404
|
return self.__class__(
|
|
329
405
|
self.raw_schema,
|
|
406
|
+
specification=self.specification,
|
|
330
407
|
location=self.location,
|
|
331
408
|
base_url=base_url, # type: ignore
|
|
332
|
-
method=method,
|
|
333
|
-
endpoint=endpoint,
|
|
334
|
-
tag=tag,
|
|
335
|
-
operation_id=operation_id,
|
|
336
409
|
app=app,
|
|
337
410
|
hooks=hooks, # type: ignore
|
|
338
411
|
auth=auth, # type: ignore
|
|
339
412
|
test_function=test_function,
|
|
340
413
|
validate_schema=validate_schema, # type: ignore
|
|
341
|
-
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
|
342
414
|
data_generation_methods=data_generation_methods, # type: ignore
|
|
343
415
|
generation_config=generation_config, # type: ignore
|
|
416
|
+
output_config=output_config, # type: ignore
|
|
344
417
|
code_sample_style=code_sample_style, # type: ignore
|
|
345
418
|
rate_limiter=rate_limiter, # type: ignore
|
|
346
419
|
sanitize_output=sanitize_output, # type: ignore
|
|
420
|
+
filter_set=filter_set, # type: ignore
|
|
421
|
+
transport=self.transport,
|
|
347
422
|
)
|
|
348
423
|
|
|
349
424
|
def get_local_hook_dispatcher(self) -> HookDispatcher | None:
|
|
@@ -400,10 +475,7 @@ class BaseSchema(Mapping):
|
|
|
400
475
|
raise NotImplementedError
|
|
401
476
|
|
|
402
477
|
def as_state_machine(self) -> type[APIStateMachine]:
|
|
403
|
-
"""Create a state machine class.
|
|
404
|
-
|
|
405
|
-
Use it for stateful testing.
|
|
406
|
-
"""
|
|
478
|
+
"""Create a state machine class."""
|
|
407
479
|
raise NotImplementedError
|
|
408
480
|
|
|
409
481
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
|
@@ -422,7 +494,10 @@ class BaseSchema(Mapping):
|
|
|
422
494
|
"""Limit the rate of sending generated requests."""
|
|
423
495
|
label = urlparse(self.base_url).netloc
|
|
424
496
|
if self.rate_limiter is not None:
|
|
425
|
-
|
|
497
|
+
if IS_PYRATE_LIMITER_ABOVE_3:
|
|
498
|
+
self.rate_limiter.try_acquire(label)
|
|
499
|
+
else:
|
|
500
|
+
return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
|
|
426
501
|
return nullcontext()
|
|
427
502
|
|
|
428
503
|
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
|
@@ -437,39 +512,33 @@ class BaseSchema(Mapping):
|
|
|
437
512
|
**kwargs: Any,
|
|
438
513
|
) -> SearchStrategy:
|
|
439
514
|
"""Build a strategy for generating test cases for all defined API operations."""
|
|
440
|
-
assert len(self.operations) > 0, "No API operations found"
|
|
441
515
|
strategies = [
|
|
442
|
-
operation.as_strategy(
|
|
516
|
+
operation.ok().as_strategy(
|
|
443
517
|
hooks=hooks,
|
|
444
518
|
auth_storage=auth_storage,
|
|
445
519
|
data_generation_method=data_generation_method,
|
|
446
520
|
generation_config=generation_config,
|
|
447
521
|
**kwargs,
|
|
448
522
|
)
|
|
449
|
-
for
|
|
450
|
-
|
|
523
|
+
for operation in self.get_all_operations(hooks=hooks)
|
|
524
|
+
if isinstance(operation, Ok)
|
|
451
525
|
]
|
|
452
526
|
return combine_strategies(strategies)
|
|
453
527
|
|
|
454
528
|
|
|
455
529
|
@dataclass
|
|
456
|
-
class APIOperationMap(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
def __setitem__(self, key: str, value: APIOperation) -> None:
|
|
460
|
-
self.data[key] = value
|
|
530
|
+
class APIOperationMap(Mapping):
|
|
531
|
+
_schema: BaseSchema
|
|
532
|
+
_data: Mapping
|
|
461
533
|
|
|
462
534
|
def __getitem__(self, item: str) -> APIOperation:
|
|
463
|
-
return self.
|
|
464
|
-
|
|
465
|
-
def __delitem__(self, key: str) -> None:
|
|
466
|
-
del self.data[key]
|
|
535
|
+
return self._data[item]
|
|
467
536
|
|
|
468
537
|
def __len__(self) -> int:
|
|
469
|
-
return len(self.
|
|
538
|
+
return len(self._data)
|
|
470
539
|
|
|
471
540
|
def __iter__(self) -> Iterator[str]:
|
|
472
|
-
return iter(self.
|
|
541
|
+
return iter(self._data)
|
|
473
542
|
|
|
474
543
|
def as_strategy(
|
|
475
544
|
self,
|
|
@@ -480,7 +549,6 @@ class APIOperationMap(MutableMapping):
|
|
|
480
549
|
**kwargs: Any,
|
|
481
550
|
) -> SearchStrategy:
|
|
482
551
|
"""Build a strategy for generating test cases for all API operations defined in this subset."""
|
|
483
|
-
assert len(self.data) > 0, "No API operations found"
|
|
484
552
|
strategies = [
|
|
485
553
|
operation.as_strategy(
|
|
486
554
|
hooks=hooks,
|
|
@@ -489,6 +557,6 @@ class APIOperationMap(MutableMapping):
|
|
|
489
557
|
generation_config=generation_config,
|
|
490
558
|
**kwargs,
|
|
491
559
|
)
|
|
492
|
-
for operation in self.
|
|
560
|
+
for operation in self._data.values()
|
|
493
561
|
]
|
|
494
562
|
return combine_strategies(strategies)
|
schemathesis/serializers.py
CHANGED
|
@@ -43,6 +43,11 @@ class Binary(str):
|
|
|
43
43
|
|
|
44
44
|
data: bytes
|
|
45
45
|
|
|
46
|
+
__slots__ = ("data",)
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash(self.data)
|
|
50
|
+
|
|
46
51
|
|
|
47
52
|
@dataclass
|
|
48
53
|
class SerializerContext:
|
|
@@ -173,7 +178,7 @@ def _to_yaml(value: Any) -> dict[str, Any]:
|
|
|
173
178
|
return {"data": yaml.dump(value, Dumper=SafeDumper)}
|
|
174
179
|
|
|
175
180
|
|
|
176
|
-
@register("text/yaml", aliases=("text/x-yaml", "
|
|
181
|
+
@register("text/yaml", aliases=("text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"))
|
|
177
182
|
class YAMLSerializer:
|
|
178
183
|
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
179
184
|
return _to_yaml(value)
|
|
@@ -241,7 +246,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
|
|
|
241
246
|
return body.getvalue()
|
|
242
247
|
|
|
243
248
|
|
|
244
|
-
@register("multipart/form-data")
|
|
249
|
+
@register("multipart/form-data", aliases=("multipart/mixed",))
|
|
245
250
|
class MultipartSerializer:
|
|
246
251
|
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
247
252
|
if isinstance(value, bytes):
|
schemathesis/service/ci.py
CHANGED
schemathesis/service/client.py
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import hashlib
|
|
3
4
|
import http
|
|
5
|
+
import json
|
|
4
6
|
from dataclasses import asdict
|
|
5
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
6
8
|
from urllib.parse import urljoin
|
|
7
9
|
|
|
8
10
|
import requests
|
|
9
11
|
from requests.adapters import HTTPAdapter, Retry
|
|
10
12
|
|
|
11
13
|
from ..constants import USER_AGENT
|
|
12
|
-
from .ci import CIProvider
|
|
13
14
|
from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
|
|
14
|
-
from .metadata import Metadata
|
|
15
|
+
from .metadata import Metadata, collect_dependency_versions
|
|
15
16
|
from .models import (
|
|
16
|
-
|
|
17
|
+
AnalysisError,
|
|
18
|
+
AnalysisResult,
|
|
19
|
+
AnalysisSuccess,
|
|
17
20
|
AuthResponse,
|
|
18
21
|
FailedUploadResponse,
|
|
19
|
-
|
|
20
|
-
UploadSource,
|
|
22
|
+
ProjectDetails,
|
|
21
23
|
ProjectEnvironment,
|
|
22
24
|
Specification,
|
|
25
|
+
UploadResponse,
|
|
26
|
+
UploadSource,
|
|
23
27
|
)
|
|
24
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ..runner import probes
|
|
31
|
+
from .ci import CIProvider
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
def response_hook(response: requests.Response, **_kwargs: Any) -> None:
|
|
27
35
|
if response.status_code != http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
@@ -98,3 +106,28 @@ class ServiceClient(requests.Session):
|
|
|
98
106
|
if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
99
107
|
return FailedUploadResponse(detail=data["detail"])
|
|
100
108
|
return UploadResponse(message=data["message"], next_url=data["next"], correlation_id=data["correlation_id"])
|
|
109
|
+
|
|
110
|
+
def analyze_schema(self, probes: list[probes.ProbeRun] | None, schema: dict[str, Any]) -> AnalysisResult:
|
|
111
|
+
"""Analyze the API schema."""
|
|
112
|
+
# Manual serialization reduces the size of the payload a bit
|
|
113
|
+
dependencies = collect_dependency_versions()
|
|
114
|
+
if probes is not None:
|
|
115
|
+
_probes = [probe.serialize() for probe in probes]
|
|
116
|
+
else:
|
|
117
|
+
_probes = []
|
|
118
|
+
content = json.dumps(
|
|
119
|
+
{
|
|
120
|
+
"probes": _probes,
|
|
121
|
+
"schema": schema,
|
|
122
|
+
"dependencies": list(map(asdict, dependencies)),
|
|
123
|
+
},
|
|
124
|
+
separators=(",", ":"),
|
|
125
|
+
)
|
|
126
|
+
response = self.post("/cli/analysis/", data=content, headers={"Content-Type": "application/json"}, timeout=None)
|
|
127
|
+
if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
|
|
128
|
+
try:
|
|
129
|
+
message = response.json()["detail"]
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
message = response.text
|
|
132
|
+
return AnalysisError(message=message)
|
|
133
|
+
return AnalysisSuccess.from_dict(response.json())
|
schemathesis/service/events.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
4
|
-
from . import ci
|
|
5
6
|
from ..exceptions import format_exception
|
|
6
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from . import ci
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
class Event:
|
|
9
13
|
"""Signalling events coming from the Schemathesis.io worker.
|