schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py
CHANGED
@@ -1,65 +1,52 @@
|
|
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
|
-
|
11
|
-
from
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
12
4
|
from dataclasses import dataclass, field
|
13
|
-
from functools import lru_cache
|
5
|
+
from functools import lru_cache, partial
|
6
|
+
from itertools import chain
|
14
7
|
from typing import (
|
8
|
+
TYPE_CHECKING,
|
15
9
|
Any,
|
16
10
|
Callable,
|
17
|
-
ContextManager,
|
18
11
|
Generator,
|
19
|
-
|
12
|
+
Generic,
|
20
13
|
Iterator,
|
21
14
|
NoReturn,
|
22
|
-
Sequence,
|
23
15
|
TypeVar,
|
24
|
-
TYPE_CHECKING,
|
25
16
|
)
|
26
|
-
from urllib.parse import quote, unquote, urljoin,
|
27
|
-
|
28
|
-
import
|
29
|
-
from
|
30
|
-
from
|
17
|
+
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
18
|
+
|
19
|
+
from schemathesis import transport
|
20
|
+
from schemathesis.core import NOT_SET, NotSet
|
21
|
+
from schemathesis.core.errors import IncorrectUsage, InvalidSchema
|
22
|
+
from schemathesis.core.output import OutputConfig
|
23
|
+
from schemathesis.core.rate_limit import build_limiter
|
24
|
+
from schemathesis.core.result import Ok, Result
|
25
|
+
from schemathesis.core.transport import Response
|
26
|
+
from schemathesis.generation import GenerationConfig, GenerationMode
|
27
|
+
from schemathesis.generation.case import Case
|
28
|
+
from schemathesis.generation.hypothesis import strategies
|
29
|
+
from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
|
30
|
+
from schemathesis.generation.meta import CaseMetadata
|
31
|
+
from schemathesis.hooks import HookDispatcherMark
|
31
32
|
|
32
|
-
from .constants import NOT_SET
|
33
|
-
from ._hypothesis import create_test
|
34
33
|
from .auths import AuthStorage
|
35
|
-
from .
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
34
|
+
from .filters import (
|
35
|
+
FilterSet,
|
36
|
+
FilterValue,
|
37
|
+
MatcherFunc,
|
38
|
+
RegexValue,
|
39
|
+
is_deprecated,
|
41
40
|
)
|
42
|
-
from .
|
43
|
-
from .hooks import HookContext, HookDispatcher, HookScope, dispatch
|
44
|
-
from .internal.result import Result, Ok
|
45
|
-
from .models import APIOperation, Case
|
46
|
-
from .stateful.state_machine import APIStateMachine
|
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
|
41
|
+
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
|
60
42
|
|
61
43
|
if TYPE_CHECKING:
|
62
|
-
from .
|
44
|
+
from hypothesis.strategies import SearchStrategy
|
45
|
+
from pyrate_limiter import Limiter
|
46
|
+
from typing_extensions import Self
|
47
|
+
|
48
|
+
from schemathesis.core import Specification
|
49
|
+
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
63
50
|
|
64
51
|
|
65
52
|
C = TypeVar("C", bound=Case)
|
@@ -70,52 +57,140 @@ def get_full_path(base_path: str, path: str) -> str:
|
|
70
57
|
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
71
58
|
|
72
59
|
|
60
|
+
@dataclass
|
61
|
+
class ApiOperationsCount:
|
62
|
+
"""Statistics about API operations."""
|
63
|
+
|
64
|
+
total: int
|
65
|
+
selected: int
|
66
|
+
|
67
|
+
__slots__ = ("total", "selected")
|
68
|
+
|
69
|
+
def __init__(self) -> None:
|
70
|
+
self.total = 0
|
71
|
+
self.selected = 0
|
72
|
+
|
73
|
+
|
73
74
|
@dataclass(eq=False)
|
74
75
|
class BaseSchema(Mapping):
|
75
76
|
raw_schema: dict[str, Any]
|
76
77
|
location: str | None = None
|
77
78
|
base_url: str | None = None
|
78
|
-
|
79
|
-
endpoint: Filter | None = None
|
80
|
-
tag: Filter | None = None
|
81
|
-
operation_id: Filter | None = None
|
79
|
+
filter_set: FilterSet = field(default_factory=FilterSet)
|
82
80
|
app: Any = None
|
83
81
|
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
84
82
|
auth: AuthStorage = field(default_factory=AuthStorage)
|
85
|
-
test_function:
|
86
|
-
validate_schema: bool = True
|
87
|
-
skip_deprecated_operations: bool = False
|
88
|
-
data_generation_methods: list[DataGenerationMethod] = field(
|
89
|
-
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
90
|
-
)
|
83
|
+
test_function: Callable | None = None
|
91
84
|
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
92
|
-
|
85
|
+
output_config: OutputConfig = field(default_factory=OutputConfig)
|
93
86
|
rate_limiter: Limiter | None = None
|
94
|
-
|
87
|
+
_operations_count: ApiOperationsCount | None = None
|
88
|
+
|
89
|
+
def __post_init__(self) -> None:
|
90
|
+
self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
|
91
|
+
|
92
|
+
@property
|
93
|
+
def specification(self) -> Specification:
|
94
|
+
raise NotImplementedError
|
95
|
+
|
96
|
+
@property
|
97
|
+
def transport(self) -> transport.BaseTransport:
|
98
|
+
return transport.get(self.app)
|
99
|
+
|
100
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
101
|
+
|
102
|
+
def include(
|
103
|
+
self,
|
104
|
+
func: MatcherFunc | None = None,
|
105
|
+
*,
|
106
|
+
name: FilterValue | None = None,
|
107
|
+
name_regex: str | None = None,
|
108
|
+
method: FilterValue | None = None,
|
109
|
+
method_regex: str | None = None,
|
110
|
+
path: FilterValue | None = None,
|
111
|
+
path_regex: str | None = None,
|
112
|
+
tag: FilterValue | None = None,
|
113
|
+
tag_regex: RegexValue | None = None,
|
114
|
+
operation_id: FilterValue | None = None,
|
115
|
+
operation_id_regex: RegexValue | None = None,
|
116
|
+
) -> BaseSchema:
|
117
|
+
"""Include only operations that match the given filters."""
|
118
|
+
filter_set = self.filter_set.clone()
|
119
|
+
filter_set.include(
|
120
|
+
func,
|
121
|
+
name=name,
|
122
|
+
name_regex=name_regex,
|
123
|
+
method=method,
|
124
|
+
method_regex=method_regex,
|
125
|
+
path=path,
|
126
|
+
path_regex=path_regex,
|
127
|
+
tag=tag,
|
128
|
+
tag_regex=tag_regex,
|
129
|
+
operation_id=operation_id,
|
130
|
+
operation_id_regex=operation_id_regex,
|
131
|
+
)
|
132
|
+
return self.clone(filter_set=filter_set)
|
133
|
+
|
134
|
+
def exclude(
|
135
|
+
self,
|
136
|
+
func: MatcherFunc | None = None,
|
137
|
+
*,
|
138
|
+
name: FilterValue | None = None,
|
139
|
+
name_regex: str | None = None,
|
140
|
+
method: FilterValue | None = None,
|
141
|
+
method_regex: str | None = None,
|
142
|
+
path: FilterValue | None = None,
|
143
|
+
path_regex: str | None = None,
|
144
|
+
tag: FilterValue | None = None,
|
145
|
+
tag_regex: RegexValue | None = None,
|
146
|
+
operation_id: FilterValue | None = None,
|
147
|
+
operation_id_regex: RegexValue | None = None,
|
148
|
+
deprecated: bool = False,
|
149
|
+
) -> BaseSchema:
|
150
|
+
"""Include only operations that match the given filters."""
|
151
|
+
filter_set = self.filter_set.clone()
|
152
|
+
if deprecated:
|
153
|
+
if func is None:
|
154
|
+
func = is_deprecated
|
155
|
+
else:
|
156
|
+
filter_set.exclude(is_deprecated)
|
157
|
+
filter_set.exclude(
|
158
|
+
func,
|
159
|
+
name=name,
|
160
|
+
name_regex=name_regex,
|
161
|
+
method=method,
|
162
|
+
method_regex=method_regex,
|
163
|
+
path=path,
|
164
|
+
path_regex=path_regex,
|
165
|
+
tag=tag,
|
166
|
+
tag_regex=tag_regex,
|
167
|
+
operation_id=operation_id,
|
168
|
+
operation_id_regex=operation_id_regex,
|
169
|
+
)
|
170
|
+
return self.clone(filter_set=filter_set)
|
95
171
|
|
96
172
|
def __iter__(self) -> Iterator[str]:
|
97
|
-
|
173
|
+
raise NotImplementedError
|
98
174
|
|
99
175
|
def __getitem__(self, item: str) -> APIOperationMap:
|
100
176
|
__tracebackhide__ = True
|
101
177
|
try:
|
102
|
-
return self.
|
178
|
+
return self._get_operation_map(item)
|
103
179
|
except KeyError as exc:
|
104
180
|
self.on_missing_operation(item, exc)
|
105
181
|
|
182
|
+
def _get_operation_map(self, key: str) -> APIOperationMap:
|
183
|
+
raise NotImplementedError
|
184
|
+
|
106
185
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
107
186
|
raise NotImplementedError
|
108
187
|
|
109
188
|
def __len__(self) -> int:
|
110
|
-
return
|
189
|
+
return self.count_operations().total
|
111
190
|
|
112
191
|
def hook(self, hook: str | Callable) -> Callable:
|
113
192
|
return self.hooks.register(hook)
|
114
193
|
|
115
|
-
@property
|
116
|
-
def verbose_name(self) -> str:
|
117
|
-
raise NotImplementedError
|
118
|
-
|
119
194
|
def get_full_path(self, path: str) -> str:
|
120
195
|
"""Compute full path for the given path."""
|
121
196
|
return get_full_path(self.base_path, path)
|
@@ -150,20 +225,14 @@ class BaseSchema(Mapping):
|
|
150
225
|
def validate(self) -> None:
|
151
226
|
raise NotImplementedError
|
152
227
|
|
153
|
-
|
154
|
-
|
155
|
-
if
|
156
|
-
|
157
|
-
|
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
|
228
|
+
def count_operations(self) -> ApiOperationsCount:
|
229
|
+
"""Count total and selected operations."""
|
230
|
+
if self._operations_count is None:
|
231
|
+
self._operations_count = self._do_count_operations()
|
232
|
+
return self._operations_count
|
164
233
|
|
165
|
-
|
166
|
-
|
234
|
+
def _do_count_operations(self) -> ApiOperationsCount:
|
235
|
+
"""Implementation-specific counting logic."""
|
167
236
|
raise NotImplementedError
|
168
237
|
|
169
238
|
@property
|
@@ -171,11 +240,11 @@ class BaseSchema(Mapping):
|
|
171
240
|
raise NotImplementedError
|
172
241
|
|
173
242
|
def get_all_operations(
|
174
|
-
self,
|
175
|
-
) -> Generator[Result[APIOperation,
|
243
|
+
self, generation_config: GenerationConfig | None = None
|
244
|
+
) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
176
245
|
raise NotImplementedError
|
177
246
|
|
178
|
-
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
247
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
179
248
|
"""Get examples from the API operation."""
|
180
249
|
raise NotImplementedError
|
181
250
|
|
@@ -183,70 +252,20 @@ class BaseSchema(Mapping):
|
|
183
252
|
"""Get applied security requirements for the given API operation."""
|
184
253
|
raise NotImplementedError
|
185
254
|
|
186
|
-
def get_stateful_tests(
|
187
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
188
|
-
) -> Sequence[StatefulTest]:
|
189
|
-
"""Get a list of additional tests, that should be executed after this response from the API operation."""
|
190
|
-
raise NotImplementedError
|
191
|
-
|
192
255
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
193
256
|
"""Get a function that serializes parameters for the given location."""
|
194
257
|
raise NotImplementedError
|
195
258
|
|
196
|
-
def
|
197
|
-
self,
|
198
|
-
func: Callable,
|
199
|
-
settings: hypothesis.settings | None = None,
|
200
|
-
generation_config: GenerationConfig | None = None,
|
201
|
-
seed: int | None = None,
|
202
|
-
as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None = None,
|
203
|
-
hooks: HookDispatcher | None = None,
|
204
|
-
_given_kwargs: dict[str, GivenInput] | None = None,
|
205
|
-
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
206
|
-
"""Generate all operations and Hypothesis tests for them."""
|
207
|
-
for result in self.get_all_operations(hooks=hooks):
|
208
|
-
if isinstance(result, Ok):
|
209
|
-
operation = result.ok()
|
210
|
-
_as_strategy_kwargs: dict[str, Any] | None
|
211
|
-
if callable(as_strategy_kwargs):
|
212
|
-
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
213
|
-
else:
|
214
|
-
_as_strategy_kwargs = as_strategy_kwargs
|
215
|
-
test = create_test(
|
216
|
-
operation=operation,
|
217
|
-
test=func,
|
218
|
-
settings=settings,
|
219
|
-
seed=seed,
|
220
|
-
data_generation_methods=self.data_generation_methods,
|
221
|
-
generation_config=generation_config,
|
222
|
-
as_strategy_kwargs=_as_strategy_kwargs,
|
223
|
-
_given_kwargs=_given_kwargs,
|
224
|
-
)
|
225
|
-
yield Ok((operation, test))
|
226
|
-
else:
|
227
|
-
yield result
|
228
|
-
|
229
|
-
def parametrize(
|
230
|
-
self,
|
231
|
-
method: Filter | None = NOT_SET,
|
232
|
-
endpoint: Filter | None = NOT_SET,
|
233
|
-
tag: Filter | None = NOT_SET,
|
234
|
-
operation_id: Filter | None = NOT_SET,
|
235
|
-
validate_schema: bool | NotSet = NOT_SET,
|
236
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
237
|
-
data_generation_methods: Iterable[DataGenerationMethod] | NotSet = NOT_SET,
|
238
|
-
code_sample_style: str | NotSet = NOT_SET,
|
239
|
-
) -> Callable:
|
259
|
+
def parametrize(self) -> Callable:
|
240
260
|
"""Mark a test function as a parametrized one."""
|
241
|
-
_code_sample_style = (
|
242
|
-
CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
|
243
|
-
)
|
244
261
|
|
245
|
-
def wrapper(func:
|
246
|
-
|
262
|
+
def wrapper(func: Callable) -> Callable:
|
263
|
+
from schemathesis.pytest.plugin import SchemaHandleMark
|
264
|
+
|
265
|
+
if SchemaHandleMark.is_set(func):
|
247
266
|
|
248
267
|
def wrapped_test(*_: Any, **__: Any) -> NoReturn:
|
249
|
-
raise
|
268
|
+
raise IncorrectUsage(
|
250
269
|
f"You have applied `parametrize` to the `{func.__name__}` test more than once, which "
|
251
270
|
"overrides the previous decorator. "
|
252
271
|
"The `parametrize` decorator could be applied to the same function at most once."
|
@@ -254,18 +273,8 @@ class BaseSchema(Mapping):
|
|
254
273
|
|
255
274
|
return wrapped_test
|
256
275
|
HookDispatcher.add_dispatcher(func)
|
257
|
-
cloned = self.clone(
|
258
|
-
|
259
|
-
method=method,
|
260
|
-
endpoint=endpoint,
|
261
|
-
tag=tag,
|
262
|
-
operation_id=operation_id,
|
263
|
-
validate_schema=validate_schema,
|
264
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
265
|
-
data_generation_methods=data_generation_methods,
|
266
|
-
code_sample_style=_code_sample_style, # type: ignore
|
267
|
-
)
|
268
|
-
setattr(func, PARAMETRIZE_MARKER, cloned)
|
276
|
+
cloned = self.clone(test_function=func)
|
277
|
+
SchemaHandleMark.set(func, cloned)
|
269
278
|
return func
|
270
279
|
|
271
280
|
return wrapper
|
@@ -275,75 +284,29 @@ class BaseSchema(Mapping):
|
|
275
284
|
return given_proxy(*args, **kwargs)
|
276
285
|
|
277
286
|
def clone(
|
278
|
-
self,
|
279
|
-
*,
|
280
|
-
base_url: str | None | NotSet = NOT_SET,
|
281
|
-
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
|
-
app: Any = NOT_SET,
|
287
|
-
hooks: HookDispatcher | NotSet = NOT_SET,
|
288
|
-
auth: AuthStorage | NotSet = NOT_SET,
|
289
|
-
validate_schema: bool | NotSet = NOT_SET,
|
290
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
291
|
-
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
292
|
-
generation_config: GenerationConfig | NotSet = NOT_SET,
|
293
|
-
code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
|
294
|
-
rate_limiter: Limiter | None = NOT_SET,
|
295
|
-
sanitize_output: bool | NotSet | None = NOT_SET,
|
287
|
+
self, *, test_function: Callable | NotSet = NOT_SET, filter_set: FilterSet | NotSet = NOT_SET
|
296
288
|
) -> BaseSchema:
|
297
|
-
if
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
if
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
if operation_id is NOT_SET:
|
306
|
-
operation_id = self.operation_id
|
307
|
-
if app is NOT_SET:
|
308
|
-
app = self.app
|
309
|
-
if validate_schema is NOT_SET:
|
310
|
-
validate_schema = self.validate_schema
|
311
|
-
if skip_deprecated_operations is NOT_SET:
|
312
|
-
skip_deprecated_operations = self.skip_deprecated_operations
|
313
|
-
if hooks is NOT_SET:
|
314
|
-
hooks = self.hooks
|
315
|
-
if auth is NOT_SET:
|
316
|
-
auth = self.auth
|
317
|
-
if data_generation_methods is NOT_SET:
|
318
|
-
data_generation_methods = self.data_generation_methods
|
319
|
-
if generation_config is NOT_SET:
|
320
|
-
generation_config = self.generation_config
|
321
|
-
if code_sample_style is NOT_SET:
|
322
|
-
code_sample_style = self.code_sample_style
|
323
|
-
if rate_limiter is NOT_SET:
|
324
|
-
rate_limiter = self.rate_limiter
|
325
|
-
if sanitize_output is NOT_SET:
|
326
|
-
sanitize_output = self.sanitize_output
|
289
|
+
if isinstance(test_function, NotSet):
|
290
|
+
_test_function = self.test_function
|
291
|
+
else:
|
292
|
+
_test_function = test_function
|
293
|
+
if isinstance(filter_set, NotSet):
|
294
|
+
_filter_set = self.filter_set
|
295
|
+
else:
|
296
|
+
_filter_set = filter_set
|
327
297
|
|
328
298
|
return self.__class__(
|
329
299
|
self.raw_schema,
|
330
300
|
location=self.location,
|
331
|
-
base_url=base_url,
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
validate_schema=validate_schema, # type: ignore
|
341
|
-
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
342
|
-
data_generation_methods=data_generation_methods, # type: ignore
|
343
|
-
generation_config=generation_config, # type: ignore
|
344
|
-
code_sample_style=code_sample_style, # type: ignore
|
345
|
-
rate_limiter=rate_limiter, # type: ignore
|
346
|
-
sanitize_output=sanitize_output, # type: ignore
|
301
|
+
base_url=self.base_url,
|
302
|
+
app=self.app,
|
303
|
+
hooks=self.hooks,
|
304
|
+
auth=self.auth,
|
305
|
+
test_function=_test_function,
|
306
|
+
generation_config=self.generation_config,
|
307
|
+
output_config=self.output_config,
|
308
|
+
rate_limiter=self.rate_limiter,
|
309
|
+
filter_set=_filter_set,
|
347
310
|
)
|
348
311
|
|
349
312
|
def get_local_hook_dispatcher(self) -> HookDispatcher | None:
|
@@ -351,7 +314,7 @@ class BaseSchema(Mapping):
|
|
351
314
|
# It might be not present when it is used without pytest via `APIOperation.as_strategy()`
|
352
315
|
if self.test_function is not None:
|
353
316
|
# Might be missing it in case of `LazySchema` usage
|
354
|
-
return
|
317
|
+
return HookDispatcherMark.get(self.test_function)
|
355
318
|
return None
|
356
319
|
|
357
320
|
def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
|
@@ -363,7 +326,7 @@ class BaseSchema(Mapping):
|
|
363
326
|
local_dispatcher.dispatch(name, context, *args, **kwargs)
|
364
327
|
|
365
328
|
def prepare_multipart(
|
366
|
-
self, form_data:
|
329
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
367
330
|
) -> tuple[list | None, dict[str, Any] | None]:
|
368
331
|
"""Split content of `form_data` into files & data.
|
369
332
|
|
@@ -377,15 +340,17 @@ class BaseSchema(Mapping):
|
|
377
340
|
def make_case(
|
378
341
|
self,
|
379
342
|
*,
|
380
|
-
case_cls: type[C],
|
381
343
|
operation: APIOperation,
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
344
|
+
method: str | None = None,
|
345
|
+
path: str | None = None,
|
346
|
+
path_parameters: dict[str, Any] | None = None,
|
347
|
+
headers: dict[str, Any] | None = None,
|
348
|
+
cookies: dict[str, Any] | None = None,
|
349
|
+
query: dict[str, Any] | None = None,
|
350
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
387
351
|
media_type: str | None = None,
|
388
|
-
|
352
|
+
meta: CaseMetadata | None = None,
|
353
|
+
) -> Case:
|
389
354
|
raise NotImplementedError
|
390
355
|
|
391
356
|
def get_case_strategy(
|
@@ -393,17 +358,14 @@ class BaseSchema(Mapping):
|
|
393
358
|
operation: APIOperation,
|
394
359
|
hooks: HookDispatcher | None = None,
|
395
360
|
auth_storage: AuthStorage | None = None,
|
396
|
-
|
361
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
397
362
|
generation_config: GenerationConfig | None = None,
|
398
363
|
**kwargs: Any,
|
399
364
|
) -> SearchStrategy:
|
400
365
|
raise NotImplementedError
|
401
366
|
|
402
367
|
def as_state_machine(self) -> type[APIStateMachine]:
|
403
|
-
"""Create a state machine class.
|
404
|
-
|
405
|
-
Use it for stateful testing.
|
406
|
-
"""
|
368
|
+
"""Create a state machine class."""
|
407
369
|
raise NotImplementedError
|
408
370
|
|
409
371
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
@@ -412,19 +374,12 @@ class BaseSchema(Mapping):
|
|
412
374
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
413
375
|
raise NotImplementedError
|
414
376
|
|
415
|
-
def validate_response(self, operation: APIOperation, response:
|
377
|
+
def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
|
416
378
|
raise NotImplementedError
|
417
379
|
|
418
380
|
def prepare_schema(self, schema: Any) -> Any:
|
419
381
|
raise NotImplementedError
|
420
382
|
|
421
|
-
def ratelimit(self) -> ContextManager:
|
422
|
-
"""Limit the rate of sending generated requests."""
|
423
|
-
label = urlparse(self.base_url).netloc
|
424
|
-
if self.rate_limiter is not None:
|
425
|
-
return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
|
426
|
-
return nullcontext()
|
427
|
-
|
428
383
|
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
429
384
|
raise NotImplementedError
|
430
385
|
|
@@ -432,63 +387,384 @@ class BaseSchema(Mapping):
|
|
432
387
|
self,
|
433
388
|
hooks: HookDispatcher | None = None,
|
434
389
|
auth_storage: AuthStorage | None = None,
|
435
|
-
|
390
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
436
391
|
generation_config: GenerationConfig | None = None,
|
437
392
|
**kwargs: Any,
|
438
393
|
) -> SearchStrategy:
|
439
394
|
"""Build a strategy for generating test cases for all defined API operations."""
|
440
|
-
|
441
|
-
|
442
|
-
operation.as_strategy(
|
395
|
+
_strategies = [
|
396
|
+
operation.ok().as_strategy(
|
443
397
|
hooks=hooks,
|
444
398
|
auth_storage=auth_storage,
|
445
|
-
|
399
|
+
generation_mode=generation_mode,
|
446
400
|
generation_config=generation_config,
|
447
401
|
**kwargs,
|
448
402
|
)
|
449
|
-
for
|
450
|
-
|
403
|
+
for operation in self.get_all_operations()
|
404
|
+
if isinstance(operation, Ok)
|
451
405
|
]
|
452
|
-
return
|
406
|
+
return strategies.combine(_strategies)
|
453
407
|
|
408
|
+
def configure(
|
409
|
+
self,
|
410
|
+
*,
|
411
|
+
base_url: str | None | NotSet = NOT_SET,
|
412
|
+
location: str | None | NotSet = NOT_SET,
|
413
|
+
rate_limit: str | None | NotSet = NOT_SET,
|
414
|
+
generation: GenerationConfig | NotSet = NOT_SET,
|
415
|
+
output: OutputConfig | NotSet = NOT_SET,
|
416
|
+
app: Any | NotSet = NOT_SET,
|
417
|
+
) -> Self:
|
418
|
+
if not isinstance(base_url, NotSet):
|
419
|
+
self.base_url = base_url
|
420
|
+
if not isinstance(location, NotSet):
|
421
|
+
self.location = location
|
422
|
+
if not isinstance(rate_limit, NotSet):
|
423
|
+
if isinstance(rate_limit, str):
|
424
|
+
self.rate_limiter = build_limiter(rate_limit)
|
425
|
+
else:
|
426
|
+
self.rate_limiter = None
|
427
|
+
if not isinstance(generation, NotSet):
|
428
|
+
self.generation_config = generation
|
429
|
+
if not isinstance(output, NotSet):
|
430
|
+
self.output_config = output
|
431
|
+
if not isinstance(app, NotSet):
|
432
|
+
self.app = app
|
433
|
+
return self
|
454
434
|
|
455
|
-
@dataclass
|
456
|
-
class APIOperationMap(MutableMapping):
|
457
|
-
data: MutableMapping
|
458
435
|
|
459
|
-
|
460
|
-
|
436
|
+
@dataclass
|
437
|
+
class APIOperationMap(Mapping):
|
438
|
+
_schema: BaseSchema
|
439
|
+
_data: Mapping
|
461
440
|
|
462
441
|
def __getitem__(self, item: str) -> APIOperation:
|
463
|
-
return self.
|
464
|
-
|
465
|
-
def __delitem__(self, key: str) -> None:
|
466
|
-
del self.data[key]
|
442
|
+
return self._data[item]
|
467
443
|
|
468
444
|
def __len__(self) -> int:
|
469
|
-
return len(self.
|
445
|
+
return len(self._data)
|
470
446
|
|
471
447
|
def __iter__(self) -> Iterator[str]:
|
472
|
-
return iter(self.
|
448
|
+
return iter(self._data)
|
473
449
|
|
474
450
|
def as_strategy(
|
475
451
|
self,
|
476
452
|
hooks: HookDispatcher | None = None,
|
477
453
|
auth_storage: AuthStorage | None = None,
|
478
|
-
|
454
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
479
455
|
generation_config: GenerationConfig | None = None,
|
480
456
|
**kwargs: Any,
|
481
457
|
) -> SearchStrategy:
|
482
458
|
"""Build a strategy for generating test cases for all API operations defined in this subset."""
|
483
|
-
|
484
|
-
strategies = [
|
459
|
+
_strategies = [
|
485
460
|
operation.as_strategy(
|
486
461
|
hooks=hooks,
|
487
462
|
auth_storage=auth_storage,
|
488
|
-
|
463
|
+
generation_mode=generation_mode,
|
489
464
|
generation_config=generation_config,
|
490
465
|
**kwargs,
|
491
466
|
)
|
492
|
-
for operation in self.
|
467
|
+
for operation in self._data.values()
|
493
468
|
]
|
494
|
-
return
|
469
|
+
return strategies.combine(_strategies)
|
470
|
+
|
471
|
+
|
472
|
+
@dataclass(eq=False)
|
473
|
+
class Parameter:
|
474
|
+
"""A logically separate parameter bound to a location (e.g., to "query string").
|
475
|
+
|
476
|
+
For example, if the API requires multiple headers to be present, each header is presented as a separate
|
477
|
+
`Parameter` instance.
|
478
|
+
"""
|
479
|
+
|
480
|
+
# The parameter definition in the language acceptable by the API
|
481
|
+
definition: Any
|
482
|
+
|
483
|
+
@property
|
484
|
+
def location(self) -> str:
|
485
|
+
"""Where this parameter is located.
|
486
|
+
|
487
|
+
E.g. "query" or "body"
|
488
|
+
"""
|
489
|
+
raise NotImplementedError
|
490
|
+
|
491
|
+
@property
|
492
|
+
def name(self) -> str:
|
493
|
+
"""Parameter name."""
|
494
|
+
raise NotImplementedError
|
495
|
+
|
496
|
+
@property
|
497
|
+
def is_required(self) -> bool:
|
498
|
+
"""Whether the parameter is required for a successful API call."""
|
499
|
+
raise NotImplementedError
|
500
|
+
|
501
|
+
def serialize(self, operation: APIOperation) -> str:
|
502
|
+
"""Get parameter's string representation."""
|
503
|
+
raise NotImplementedError
|
504
|
+
|
505
|
+
|
506
|
+
P = TypeVar("P", bound=Parameter)
|
507
|
+
|
508
|
+
|
509
|
+
@dataclass
|
510
|
+
class ParameterSet(Generic[P]):
|
511
|
+
"""A set of parameters for the same location."""
|
512
|
+
|
513
|
+
items: list[P] = field(default_factory=list)
|
514
|
+
|
515
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
516
|
+
|
517
|
+
def add(self, parameter: P) -> None:
|
518
|
+
"""Add a new parameter."""
|
519
|
+
self.items.append(parameter)
|
520
|
+
|
521
|
+
def get(self, name: str) -> P | None:
|
522
|
+
for parameter in self:
|
523
|
+
if parameter.name == name:
|
524
|
+
return parameter
|
525
|
+
return None
|
526
|
+
|
527
|
+
def contains(self, name: str) -> bool:
|
528
|
+
return self.get(name) is not None
|
529
|
+
|
530
|
+
def __contains__(self, item: str) -> bool:
|
531
|
+
return self.contains(item)
|
532
|
+
|
533
|
+
def __bool__(self) -> bool:
|
534
|
+
return bool(self.items)
|
535
|
+
|
536
|
+
def __iter__(self) -> Generator[P, None, None]:
|
537
|
+
yield from iter(self.items)
|
538
|
+
|
539
|
+
def __len__(self) -> int:
|
540
|
+
return len(self.items)
|
541
|
+
|
542
|
+
def __getitem__(self, item: int) -> P:
|
543
|
+
return self.items[item]
|
544
|
+
|
545
|
+
|
546
|
+
class PayloadAlternatives(ParameterSet[P]):
|
547
|
+
"""A set of alternative payloads."""
|
548
|
+
|
549
|
+
|
550
|
+
D = TypeVar("D", bound=dict)
|
551
|
+
|
552
|
+
|
553
|
+
@dataclass(repr=False)
|
554
|
+
class OperationDefinition(Generic[D]):
|
555
|
+
"""A wrapper to store not resolved API operation definitions.
|
556
|
+
|
557
|
+
To prevent recursion errors we need to store definitions without resolving references. But operation definitions
|
558
|
+
itself can be behind a reference (when there is a ``$ref`` in ``paths`` values), therefore we need to store this
|
559
|
+
scope change to have a proper reference resolving later.
|
560
|
+
"""
|
561
|
+
|
562
|
+
raw: D
|
563
|
+
resolved: D
|
564
|
+
scope: str
|
565
|
+
|
566
|
+
__slots__ = ("raw", "resolved", "scope")
|
567
|
+
|
568
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
569
|
+
|
570
|
+
|
571
|
+
@dataclass(eq=False)
|
572
|
+
class APIOperation(Generic[P]):
|
573
|
+
"""A single operation defined in an API.
|
574
|
+
|
575
|
+
You can get one via a ``schema`` instance.
|
576
|
+
|
577
|
+
.. code-block:: python
|
578
|
+
|
579
|
+
# Get the POST /items operation
|
580
|
+
operation = schema["/items"]["POST"]
|
581
|
+
|
582
|
+
"""
|
583
|
+
|
584
|
+
# `path` does not contain `basePath`
|
585
|
+
# Example <scheme>://<host>/<basePath>/users - "/users" is path
|
586
|
+
# https://swagger.io/docs/specification/2-0/api-host-and-base-path/
|
587
|
+
path: str
|
588
|
+
method: str
|
589
|
+
definition: OperationDefinition = field(repr=False)
|
590
|
+
schema: BaseSchema
|
591
|
+
label: str = None # type: ignore
|
592
|
+
app: Any = None
|
593
|
+
base_url: str | None = None
|
594
|
+
path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
|
595
|
+
headers: ParameterSet[P] = field(default_factory=ParameterSet)
|
596
|
+
cookies: ParameterSet[P] = field(default_factory=ParameterSet)
|
597
|
+
query: ParameterSet[P] = field(default_factory=ParameterSet)
|
598
|
+
body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
|
599
|
+
|
600
|
+
def __post_init__(self) -> None:
|
601
|
+
if self.label is None:
|
602
|
+
self.label = f"{self.method.upper()} {self.full_path}" # type: ignore
|
603
|
+
|
604
|
+
@property
|
605
|
+
def full_path(self) -> str:
|
606
|
+
return self.schema.get_full_path(self.path)
|
607
|
+
|
608
|
+
@property
|
609
|
+
def links(self) -> dict[str, dict[str, Any]]:
|
610
|
+
return self.schema.get_links(self)
|
611
|
+
|
612
|
+
@property
|
613
|
+
def tags(self) -> list[str] | None:
|
614
|
+
return self.schema.get_tags(self)
|
615
|
+
|
616
|
+
def iter_parameters(self) -> Iterator[P]:
|
617
|
+
"""Iterate over all operation's parameters."""
|
618
|
+
return chain(self.path_parameters, self.headers, self.cookies, self.query)
|
619
|
+
|
620
|
+
def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
|
621
|
+
return {
|
622
|
+
"path": self.path_parameters,
|
623
|
+
"header": self.headers,
|
624
|
+
"cookie": self.cookies,
|
625
|
+
"query": self.query,
|
626
|
+
"body": self.body,
|
627
|
+
}.get(location)
|
628
|
+
|
629
|
+
def add_parameter(self, parameter: P) -> None:
|
630
|
+
"""Add a new processed parameter to an API operation.
|
631
|
+
|
632
|
+
:param parameter: A parameter that will be used with this operation.
|
633
|
+
:rtype: None
|
634
|
+
"""
|
635
|
+
# If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
|
636
|
+
# But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
|
637
|
+
# In this case, we still might generate some tests for an API operation, but without this parameter,
|
638
|
+
# which is better than skip the whole operation from testing.
|
639
|
+
container = self._lookup_container(parameter.location)
|
640
|
+
if container is not None:
|
641
|
+
container.add(parameter)
|
642
|
+
|
643
|
+
def get_parameter(self, name: str, location: str) -> P | None:
|
644
|
+
container = self._lookup_container(location)
|
645
|
+
if container is not None:
|
646
|
+
return container.get(name)
|
647
|
+
return None
|
648
|
+
|
649
|
+
def as_strategy(
|
650
|
+
self,
|
651
|
+
hooks: HookDispatcher | None = None,
|
652
|
+
auth_storage: AuthStorage | None = None,
|
653
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
654
|
+
generation_config: GenerationConfig | None = None,
|
655
|
+
**kwargs: Any,
|
656
|
+
) -> SearchStrategy[Case]:
|
657
|
+
"""Turn this API operation into a Hypothesis strategy."""
|
658
|
+
strategy = self.schema.get_case_strategy(
|
659
|
+
self, hooks, auth_storage, generation_mode, generation_config=generation_config, **kwargs
|
660
|
+
)
|
661
|
+
|
662
|
+
def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
|
663
|
+
context = HookContext(self)
|
664
|
+
for hook in dispatcher.get_all_by_name("before_generate_case"):
|
665
|
+
_strategy = hook(context, _strategy)
|
666
|
+
for hook in dispatcher.get_all_by_name("filter_case"):
|
667
|
+
hook = partial(hook, context)
|
668
|
+
_strategy = _strategy.filter(hook)
|
669
|
+
for hook in dispatcher.get_all_by_name("map_case"):
|
670
|
+
hook = partial(hook, context)
|
671
|
+
_strategy = _strategy.map(hook)
|
672
|
+
for hook in dispatcher.get_all_by_name("flatmap_case"):
|
673
|
+
hook = partial(hook, context)
|
674
|
+
_strategy = _strategy.flatmap(hook)
|
675
|
+
return _strategy
|
676
|
+
|
677
|
+
strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
|
678
|
+
strategy = _apply_hooks(self.schema.hooks, strategy)
|
679
|
+
if hooks is not None:
|
680
|
+
strategy = _apply_hooks(hooks, strategy)
|
681
|
+
return strategy
|
682
|
+
|
683
|
+
def get_security_requirements(self) -> list[str]:
|
684
|
+
return self.schema.get_security_requirements(self)
|
685
|
+
|
686
|
+
def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
687
|
+
"""Get examples from the API operation."""
|
688
|
+
kwargs.setdefault("generation_config", self.schema.generation_config)
|
689
|
+
return self.schema.get_strategies_from_examples(self, **kwargs)
|
690
|
+
|
691
|
+
def get_parameter_serializer(self, location: str) -> Callable | None:
|
692
|
+
"""Get a function that serializes parameters for the given location.
|
693
|
+
|
694
|
+
It handles serializing data into various `collectionFormat` options and similar.
|
695
|
+
Note that payload is handled by this function - it is handled by serializers.
|
696
|
+
"""
|
697
|
+
return self.schema.get_parameter_serializer(self, location)
|
698
|
+
|
699
|
+
def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
|
700
|
+
return self.schema.prepare_multipart(form_data, self)
|
701
|
+
|
702
|
+
def get_request_payload_content_types(self) -> list[str]:
|
703
|
+
return self.schema.get_request_payload_content_types(self)
|
704
|
+
|
705
|
+
def _get_default_media_type(self) -> str:
|
706
|
+
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
707
|
+
media_types = self.get_request_payload_content_types()
|
708
|
+
if len(media_types) == 1:
|
709
|
+
# The only available option
|
710
|
+
return media_types[0]
|
711
|
+
media_types_repr = ", ".join(media_types)
|
712
|
+
raise IncorrectUsage(
|
713
|
+
"Can not detect appropriate media type. "
|
714
|
+
"You can either specify one of the defined media types "
|
715
|
+
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
716
|
+
)
|
717
|
+
|
718
|
+
def Case(
|
719
|
+
self,
|
720
|
+
*,
|
721
|
+
method: str | None = None,
|
722
|
+
path_parameters: dict[str, Any] | None = None,
|
723
|
+
headers: dict[str, Any] | None = None,
|
724
|
+
cookies: dict[str, Any] | None = None,
|
725
|
+
query: dict[str, Any] | None = None,
|
726
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
727
|
+
media_type: str | None = None,
|
728
|
+
meta: CaseMetadata | None = None,
|
729
|
+
) -> Case:
|
730
|
+
"""Create a new example for this API operation.
|
731
|
+
|
732
|
+
The main use case is constructing Case instances completely manually, without data generation.
|
733
|
+
"""
|
734
|
+
return self.schema.make_case(
|
735
|
+
operation=self,
|
736
|
+
method=method,
|
737
|
+
path_parameters=path_parameters,
|
738
|
+
headers=headers,
|
739
|
+
cookies=cookies,
|
740
|
+
query=query,
|
741
|
+
body=body,
|
742
|
+
media_type=media_type,
|
743
|
+
meta=meta,
|
744
|
+
)
|
745
|
+
|
746
|
+
@property
|
747
|
+
def operation_reference(self) -> str:
|
748
|
+
path = self.path.replace("~", "~0").replace("/", "~1")
|
749
|
+
return f"#/paths/{path}/{self.method}"
|
750
|
+
|
751
|
+
def validate_response(self, response: Response) -> bool | None:
|
752
|
+
"""Validate API response for conformance.
|
753
|
+
|
754
|
+
:raises FailureGroup: If the response does not conform to the API schema.
|
755
|
+
"""
|
756
|
+
return self.schema.validate_response(self, response)
|
757
|
+
|
758
|
+
def is_response_valid(self, response: Response) -> bool:
|
759
|
+
"""Validate API response for conformance."""
|
760
|
+
try:
|
761
|
+
self.validate_response(response)
|
762
|
+
return True
|
763
|
+
except AssertionError:
|
764
|
+
return False
|
765
|
+
|
766
|
+
def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
767
|
+
return self.schema._get_payload_schema(self.definition.raw, media_type)
|
768
|
+
|
769
|
+
def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
770
|
+
return self.schema._get_payload_schema(self.definition.resolved, media_type)
|