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