schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -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 +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -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/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -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 +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- 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 +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- 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 -920
- 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 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- 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/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- 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 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- 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 -88
- schemathesis/runner/impl/core.py +0 -1280
- 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/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- 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/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- 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.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/schemas.py
CHANGED
@@ -1,72 +1,53 @@
|
|
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.config import ProjectConfig
|
21
|
+
from schemathesis.core import NOT_SET, NotSet
|
22
|
+
from schemathesis.core.errors import IncorrectUsage, InvalidSchema
|
23
|
+
from schemathesis.core.result import Ok, Result
|
24
|
+
from schemathesis.core.transport import Response
|
25
|
+
from schemathesis.generation import GenerationMode
|
26
|
+
from schemathesis.generation.case import Case
|
27
|
+
from schemathesis.generation.hypothesis import strategies
|
28
|
+
from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
|
29
|
+
from schemathesis.generation.meta import CaseMetadata
|
30
|
+
from schemathesis.hooks import HookDispatcherMark
|
20
31
|
|
21
|
-
from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
|
22
|
-
from ._hypothesis import create_test
|
23
32
|
from .auths import AuthStorage
|
24
|
-
from .code_samples import CodeSampleStyle
|
25
|
-
from .constants import NOT_SET
|
26
|
-
from .exceptions import OperationSchemaError, UsageError
|
27
33
|
from .filters import (
|
28
34
|
FilterSet,
|
29
35
|
FilterValue,
|
30
36
|
MatcherFunc,
|
31
37
|
RegexValue,
|
32
|
-
filter_set_from_components,
|
33
38
|
is_deprecated,
|
34
39
|
)
|
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
|
40
|
+
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
|
48
41
|
|
49
42
|
if TYPE_CHECKING:
|
50
|
-
import
|
43
|
+
import httpx
|
44
|
+
import requests
|
51
45
|
from hypothesis.strategies import SearchStrategy
|
52
|
-
from
|
53
|
-
|
54
|
-
|
55
|
-
from .
|
56
|
-
from .
|
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
|
-
)
|
46
|
+
from requests.structures import CaseInsensitiveDict
|
47
|
+
from werkzeug.test import TestResponse
|
48
|
+
|
49
|
+
from schemathesis.core import Specification
|
50
|
+
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
70
51
|
|
71
52
|
|
72
53
|
C = TypeVar("C", bound=Case)
|
@@ -77,31 +58,70 @@ def get_full_path(base_path: str, path: str) -> str:
|
|
77
58
|
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
78
59
|
|
79
60
|
|
61
|
+
@dataclass
|
62
|
+
class FilteredCount:
|
63
|
+
"""Count of total items and those passing filters."""
|
64
|
+
|
65
|
+
total: int
|
66
|
+
selected: int
|
67
|
+
|
68
|
+
__slots__ = ("total", "selected")
|
69
|
+
|
70
|
+
def __init__(self) -> None:
|
71
|
+
self.total = 0
|
72
|
+
self.selected = 0
|
73
|
+
|
74
|
+
|
75
|
+
@dataclass
|
76
|
+
class ApiStatistic:
|
77
|
+
"""Statistics about API operations and links."""
|
78
|
+
|
79
|
+
operations: FilteredCount
|
80
|
+
links: FilteredCount
|
81
|
+
|
82
|
+
__slots__ = ("operations", "links")
|
83
|
+
|
84
|
+
def __init__(self) -> None:
|
85
|
+
self.operations = FilteredCount()
|
86
|
+
self.links = FilteredCount()
|
87
|
+
|
88
|
+
|
89
|
+
@dataclass
|
90
|
+
class ApiOperationsCount:
|
91
|
+
"""Statistics about API operations."""
|
92
|
+
|
93
|
+
total: int
|
94
|
+
selected: int
|
95
|
+
|
96
|
+
__slots__ = ("total", "selected")
|
97
|
+
|
98
|
+
def __init__(self) -> None:
|
99
|
+
self.total = 0
|
100
|
+
self.selected = 0
|
101
|
+
|
102
|
+
|
80
103
|
@dataclass(eq=False)
|
81
104
|
class BaseSchema(Mapping):
|
82
105
|
raw_schema: dict[str, Any]
|
83
|
-
|
84
|
-
specification: Specification
|
106
|
+
config: ProjectConfig
|
85
107
|
location: str | None = None
|
86
|
-
base_url: str | None = None
|
87
108
|
filter_set: FilterSet = field(default_factory=FilterSet)
|
88
109
|
app: Any = None
|
89
110
|
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
90
111
|
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
|
-
)
|
96
|
-
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
97
|
-
output_config: OutputConfig = field(default_factory=OutputConfig)
|
98
|
-
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
99
|
-
rate_limiter: Limiter | None = None
|
100
|
-
sanitize_output: bool = True
|
112
|
+
test_function: Callable | None = None
|
101
113
|
|
102
114
|
def __post_init__(self) -> None:
|
103
115
|
self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
|
104
116
|
|
117
|
+
@property
|
118
|
+
def specification(self) -> Specification:
|
119
|
+
raise NotImplementedError
|
120
|
+
|
121
|
+
@property
|
122
|
+
def transport(self) -> transport.BaseTransport:
|
123
|
+
return transport.get(self.app)
|
124
|
+
|
105
125
|
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
106
126
|
|
107
127
|
def include(
|
@@ -119,7 +139,25 @@ class BaseSchema(Mapping):
|
|
119
139
|
operation_id: FilterValue | None = None,
|
120
140
|
operation_id_regex: RegexValue | None = None,
|
121
141
|
) -> BaseSchema:
|
122
|
-
"""
|
142
|
+
"""Return a new schema containing only operations matching the specified criteria.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
func: Custom filter function that accepts operation context.
|
146
|
+
name: Operation name(s) to include.
|
147
|
+
name_regex: Regex pattern for operation names.
|
148
|
+
method: HTTP method(s) to include.
|
149
|
+
method_regex: Regex pattern for HTTP methods.
|
150
|
+
path: API path(s) to include.
|
151
|
+
path_regex: Regex pattern for API paths.
|
152
|
+
tag: OpenAPI tag(s) to include.
|
153
|
+
tag_regex: Regex pattern for OpenAPI tags.
|
154
|
+
operation_id: Operation ID(s) to include.
|
155
|
+
operation_id_regex: Regex pattern for operation IDs.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
New schema instance with applied include filters.
|
159
|
+
|
160
|
+
"""
|
123
161
|
filter_set = self.filter_set.clone()
|
124
162
|
filter_set.include(
|
125
163
|
func,
|
@@ -152,7 +190,26 @@ class BaseSchema(Mapping):
|
|
152
190
|
operation_id_regex: RegexValue | None = None,
|
153
191
|
deprecated: bool = False,
|
154
192
|
) -> BaseSchema:
|
155
|
-
"""
|
193
|
+
"""Return a new schema excluding operations matching the specified criteria.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
func: Custom filter function that accepts operation context.
|
197
|
+
name: Operation name(s) to exclude.
|
198
|
+
name_regex: Regex pattern for operation names.
|
199
|
+
method: HTTP method(s) to exclude.
|
200
|
+
method_regex: Regex pattern for HTTP methods.
|
201
|
+
path: API path(s) to exclude.
|
202
|
+
path_regex: Regex pattern for API paths.
|
203
|
+
tag: OpenAPI tag(s) to exclude.
|
204
|
+
tag_regex: Regex pattern for OpenAPI tags.
|
205
|
+
operation_id: Operation ID(s) to exclude.
|
206
|
+
operation_id_regex: Regex pattern for operation IDs.
|
207
|
+
deprecated: Whether to exclude deprecated operations.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
New schema instance with applied exclude filters.
|
211
|
+
|
212
|
+
"""
|
156
213
|
filter_set = self.filter_set.clone()
|
157
214
|
if deprecated:
|
158
215
|
if func is None:
|
@@ -191,17 +248,18 @@ class BaseSchema(Mapping):
|
|
191
248
|
raise NotImplementedError
|
192
249
|
|
193
250
|
def __len__(self) -> int:
|
194
|
-
return self.
|
251
|
+
return self.statistic.operations.total
|
195
252
|
|
196
253
|
def hook(self, hook: str | Callable) -> Callable:
|
197
|
-
|
254
|
+
"""Register a hook function for this schema only.
|
198
255
|
|
199
|
-
|
200
|
-
|
201
|
-
|
256
|
+
Args:
|
257
|
+
hook: Hook name string or hook function to register.
|
258
|
+
|
259
|
+
"""
|
260
|
+
return self.hooks.hook(hook)
|
202
261
|
|
203
262
|
def get_full_path(self, path: str) -> str:
|
204
|
-
"""Compute full path for the given path."""
|
205
263
|
return get_full_path(self.base_path, path)
|
206
264
|
|
207
265
|
@property
|
@@ -209,8 +267,8 @@ class BaseSchema(Mapping):
|
|
209
267
|
"""Base path for the schema."""
|
210
268
|
# if `base_url` is specified, then it should include base path
|
211
269
|
# Example: http://127.0.0.1:8080/api
|
212
|
-
if self.base_url:
|
213
|
-
path = urlsplit(self.base_url).path
|
270
|
+
if self.config.base_url:
|
271
|
+
path = urlsplit(self.config.base_url).path
|
214
272
|
else:
|
215
273
|
path = self._get_base_path()
|
216
274
|
if not path.endswith("/"):
|
@@ -226,7 +284,7 @@ class BaseSchema(Mapping):
|
|
226
284
|
return urlunsplit(parts)
|
227
285
|
|
228
286
|
def get_base_url(self) -> str:
|
229
|
-
base_url = self.base_url
|
287
|
+
base_url = self.config.base_url
|
230
288
|
if base_url is not None:
|
231
289
|
return base_url.rstrip("/")
|
232
290
|
return self._build_base_url()
|
@@ -234,108 +292,46 @@ class BaseSchema(Mapping):
|
|
234
292
|
def validate(self) -> None:
|
235
293
|
raise NotImplementedError
|
236
294
|
|
237
|
-
@
|
238
|
-
def
|
239
|
-
|
295
|
+
@cached_property
|
296
|
+
def statistic(self) -> ApiStatistic:
|
297
|
+
return self._measure_statistic()
|
240
298
|
|
241
|
-
|
242
|
-
def links_count(self) -> int:
|
299
|
+
def _measure_statistic(self) -> ApiStatistic:
|
243
300
|
raise NotImplementedError
|
244
301
|
|
245
|
-
def get_all_operations(
|
246
|
-
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
247
|
-
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
302
|
+
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
248
303
|
raise NotImplementedError
|
249
304
|
|
250
|
-
def get_strategies_from_examples(
|
251
|
-
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
252
|
-
) -> list[SearchStrategy[Case]]:
|
253
|
-
"""Get examples from the API operation."""
|
305
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
254
306
|
raise NotImplementedError
|
255
307
|
|
256
308
|
def get_security_requirements(self, operation: APIOperation) -> list[str]:
|
257
|
-
"""Get applied security requirements for the given API operation."""
|
258
|
-
raise NotImplementedError
|
259
|
-
|
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
309
|
raise NotImplementedError
|
265
310
|
|
266
311
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
267
|
-
"""Get a function that serializes parameters for the given location."""
|
268
312
|
raise NotImplementedError
|
269
313
|
|
270
|
-
def
|
271
|
-
|
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
|
314
|
+
def parametrize(self) -> Callable:
|
315
|
+
"""Return a decorator that marks a test function for `pytest` parametrization.
|
302
316
|
|
303
|
-
|
304
|
-
|
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:
|
314
|
-
"""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
|
-
)
|
317
|
+
The decorated test function will be parametrized with test cases generated
|
318
|
+
from the schema's API operations.
|
318
319
|
|
319
|
-
|
320
|
-
|
321
|
-
if value is not NOT_SET:
|
322
|
-
warn_filtration_arguments(name)
|
320
|
+
Returns:
|
321
|
+
Decorator function for test parametrization.
|
323
322
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
tag=tag,
|
329
|
-
operation_id=operation_id,
|
330
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
331
|
-
parent=self.filter_set,
|
332
|
-
)
|
323
|
+
Raises:
|
324
|
+
IncorrectUsage: If applied to the same function multiple times.
|
325
|
+
|
326
|
+
"""
|
333
327
|
|
334
|
-
def wrapper(func:
|
335
|
-
|
328
|
+
def wrapper(func: Callable) -> Callable:
|
329
|
+
from schemathesis.pytest.plugin import SchemaHandleMark
|
330
|
+
|
331
|
+
if SchemaHandleMark.is_set(func):
|
336
332
|
|
337
333
|
def wrapped_test(*_: Any, **__: Any) -> NoReturn:
|
338
|
-
raise
|
334
|
+
raise IncorrectUsage(
|
339
335
|
f"You have applied `parametrize` to the `{func.__name__}` test more than once, which "
|
340
336
|
"overrides the previous decorator. "
|
341
337
|
"The `parametrize` decorator could be applied to the same function at most once."
|
@@ -343,94 +339,53 @@ class BaseSchema(Mapping):
|
|
343
339
|
|
344
340
|
return wrapped_test
|
345
341
|
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)
|
342
|
+
cloned = self.clone(test_function=func)
|
343
|
+
SchemaHandleMark.set(func, cloned)
|
354
344
|
return func
|
355
345
|
|
356
346
|
return wrapper
|
357
347
|
|
358
348
|
def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
|
359
|
-
"""Proxy Hypothesis
|
349
|
+
"""Proxy to Hypothesis's `given` decorator for adding custom strategies.
|
350
|
+
|
351
|
+
Args:
|
352
|
+
*args: Positional arguments passed to `hypothesis.given`.
|
353
|
+
**kwargs: Keyword arguments passed to `hypothesis.given`.
|
354
|
+
|
355
|
+
"""
|
360
356
|
return given_proxy(*args, **kwargs)
|
361
357
|
|
362
358
|
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,
|
359
|
+
self, *, test_function: Callable | NotSet = NOT_SET, filter_set: FilterSet | NotSet = NOT_SET
|
378
360
|
) -> 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
|
361
|
+
if isinstance(test_function, NotSet):
|
362
|
+
_test_function = self.test_function
|
363
|
+
else:
|
364
|
+
_test_function = test_function
|
365
|
+
if isinstance(filter_set, NotSet):
|
366
|
+
_filter_set = self.filter_set
|
367
|
+
else:
|
368
|
+
_filter_set = filter_set
|
403
369
|
|
404
370
|
return self.__class__(
|
405
371
|
self.raw_schema,
|
406
|
-
|
372
|
+
config=self.config,
|
407
373
|
location=self.location,
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
validate_schema=validate_schema, # type: ignore
|
414
|
-
data_generation_methods=data_generation_methods, # type: ignore
|
415
|
-
generation_config=generation_config, # type: ignore
|
416
|
-
output_config=output_config, # type: ignore
|
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,
|
374
|
+
app=self.app,
|
375
|
+
hooks=self.hooks,
|
376
|
+
auth=self.auth,
|
377
|
+
test_function=_test_function,
|
378
|
+
filter_set=_filter_set,
|
422
379
|
)
|
423
380
|
|
424
381
|
def get_local_hook_dispatcher(self) -> HookDispatcher | None:
|
425
|
-
"""Get a HookDispatcher instance bound to the test if present."""
|
426
382
|
# It might be not present when it is used without pytest via `APIOperation.as_strategy()`
|
427
383
|
if self.test_function is not None:
|
428
384
|
# Might be missing it in case of `LazySchema` usage
|
429
|
-
return
|
385
|
+
return HookDispatcherMark.get(self.test_function)
|
430
386
|
return None
|
431
387
|
|
432
388
|
def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
|
433
|
-
"""Dispatch a hook via all available dispatchers."""
|
434
389
|
dispatch(name, context, *args, **kwargs)
|
435
390
|
self.hooks.dispatch(name, context, *args, **kwargs)
|
436
391
|
local_dispatcher = self.get_local_hook_dispatcher()
|
@@ -438,12 +393,8 @@ class BaseSchema(Mapping):
|
|
438
393
|
local_dispatcher.dispatch(name, context, *args, **kwargs)
|
439
394
|
|
440
395
|
def prepare_multipart(
|
441
|
-
self, form_data:
|
396
|
+
self, form_data: dict[str, Any], operation: APIOperation
|
442
397
|
) -> tuple[list | None, dict[str, Any] | None]:
|
443
|
-
"""Split content of `form_data` into files & data.
|
444
|
-
|
445
|
-
Forms may contain file fields, that we should send via `files` argument in `requests`.
|
446
|
-
"""
|
447
398
|
raise NotImplementedError
|
448
399
|
|
449
400
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
@@ -452,15 +403,17 @@ class BaseSchema(Mapping):
|
|
452
403
|
def make_case(
|
453
404
|
self,
|
454
405
|
*,
|
455
|
-
case_cls: type[C],
|
456
406
|
operation: APIOperation,
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
407
|
+
method: str | None = None,
|
408
|
+
path: str | None = None,
|
409
|
+
path_parameters: dict[str, Any] | None = None,
|
410
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
411
|
+
cookies: dict[str, Any] | None = None,
|
412
|
+
query: dict[str, Any] | None = None,
|
413
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
462
414
|
media_type: str | None = None,
|
463
|
-
|
415
|
+
meta: CaseMetadata | None = None,
|
416
|
+
) -> Case:
|
464
417
|
raise NotImplementedError
|
465
418
|
|
466
419
|
def get_case_strategy(
|
@@ -468,14 +421,18 @@ class BaseSchema(Mapping):
|
|
468
421
|
operation: APIOperation,
|
469
422
|
hooks: HookDispatcher | None = None,
|
470
423
|
auth_storage: AuthStorage | None = None,
|
471
|
-
|
472
|
-
generation_config: GenerationConfig | None = None,
|
424
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
473
425
|
**kwargs: Any,
|
474
426
|
) -> SearchStrategy:
|
475
427
|
raise NotImplementedError
|
476
428
|
|
477
429
|
def as_state_machine(self) -> type[APIStateMachine]:
|
478
|
-
"""Create a state machine class.
|
430
|
+
"""Create a state machine class for stateful testing of linked API operations.
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
APIStateMachine subclass configured for this schema.
|
434
|
+
|
435
|
+
"""
|
479
436
|
raise NotImplementedError
|
480
437
|
|
481
438
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
@@ -484,46 +441,41 @@ class BaseSchema(Mapping):
|
|
484
441
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
485
442
|
raise NotImplementedError
|
486
443
|
|
487
|
-
def validate_response(self, operation: APIOperation, response:
|
444
|
+
def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
|
488
445
|
raise NotImplementedError
|
489
446
|
|
490
447
|
def prepare_schema(self, schema: Any) -> Any:
|
491
448
|
raise NotImplementedError
|
492
449
|
|
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
450
|
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
504
451
|
raise NotImplementedError
|
505
452
|
|
506
453
|
def as_strategy(
|
507
454
|
self,
|
508
|
-
|
509
|
-
auth_storage: AuthStorage | None = None,
|
510
|
-
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
511
|
-
generation_config: GenerationConfig | None = None,
|
455
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
512
456
|
**kwargs: Any,
|
513
457
|
) -> SearchStrategy:
|
514
|
-
"""
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
for
|
458
|
+
"""Create a Hypothesis strategy that generates test cases for all schema operations.
|
459
|
+
|
460
|
+
Use with `@given` in non-Schemathesis tests.
|
461
|
+
|
462
|
+
Args:
|
463
|
+
generation_mode: Whether to generate positive or negative test data.
|
464
|
+
**kwargs: Additional keywords for each strategy.
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
Combined Hypothesis strategy for all valid operations in the schema.
|
468
|
+
|
469
|
+
"""
|
470
|
+
_strategies = [
|
471
|
+
operation.ok().as_strategy(generation_mode=generation_mode, **kwargs)
|
472
|
+
for operation in self.get_all_operations()
|
524
473
|
if isinstance(operation, Ok)
|
525
474
|
]
|
526
|
-
return
|
475
|
+
return strategies.combine(_strategies)
|
476
|
+
|
477
|
+
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
478
|
+
raise NotImplementedError
|
527
479
|
|
528
480
|
|
529
481
|
@dataclass
|
@@ -542,21 +494,333 @@ class APIOperationMap(Mapping):
|
|
542
494
|
|
543
495
|
def as_strategy(
|
544
496
|
self,
|
545
|
-
|
546
|
-
auth_storage: AuthStorage | None = None,
|
547
|
-
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
548
|
-
generation_config: GenerationConfig | None = None,
|
497
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
549
498
|
**kwargs: Any,
|
550
499
|
) -> SearchStrategy:
|
551
|
-
"""
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
for
|
500
|
+
"""Create a Hypothesis strategy that generates test cases for all schema operations in this subset.
|
501
|
+
|
502
|
+
Use with `@given` in non-Schemathesis tests.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
generation_mode: Whether to generate positive or negative test data.
|
506
|
+
**kwargs: Additional keywords for each strategy.
|
507
|
+
|
508
|
+
Returns:
|
509
|
+
Combined Hypothesis strategy for all valid operations in the schema.
|
510
|
+
|
511
|
+
"""
|
512
|
+
_strategies = [
|
513
|
+
operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
|
561
514
|
]
|
562
|
-
return
|
515
|
+
return strategies.combine(_strategies)
|
516
|
+
|
517
|
+
|
518
|
+
@dataclass(eq=False)
|
519
|
+
class Parameter:
|
520
|
+
"""A logically separate parameter bound to a location (e.g., to "query string").
|
521
|
+
|
522
|
+
For example, if the API requires multiple headers to be present, each header is presented as a separate
|
523
|
+
`Parameter` instance.
|
524
|
+
"""
|
525
|
+
|
526
|
+
# The parameter definition in the language acceptable by the API
|
527
|
+
definition: Any
|
528
|
+
|
529
|
+
@property
|
530
|
+
def location(self) -> str:
|
531
|
+
"""Where this parameter is located.
|
532
|
+
|
533
|
+
E.g. "query" or "body"
|
534
|
+
"""
|
535
|
+
raise NotImplementedError
|
536
|
+
|
537
|
+
@property
|
538
|
+
def name(self) -> str:
|
539
|
+
"""Parameter name."""
|
540
|
+
raise NotImplementedError
|
541
|
+
|
542
|
+
@property
|
543
|
+
def is_required(self) -> bool:
|
544
|
+
"""Whether the parameter is required for a successful API call."""
|
545
|
+
raise NotImplementedError
|
546
|
+
|
547
|
+
def serialize(self, operation: APIOperation) -> str:
|
548
|
+
"""Get parameter's string representation."""
|
549
|
+
raise NotImplementedError
|
550
|
+
|
551
|
+
|
552
|
+
P = TypeVar("P", bound=Parameter)
|
553
|
+
|
554
|
+
|
555
|
+
@dataclass
|
556
|
+
class ParameterSet(Generic[P]):
|
557
|
+
"""A set of parameters for the same location."""
|
558
|
+
|
559
|
+
items: list[P] = field(default_factory=list)
|
560
|
+
|
561
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
562
|
+
|
563
|
+
def add(self, parameter: P) -> None:
|
564
|
+
"""Add a new parameter."""
|
565
|
+
self.items.append(parameter)
|
566
|
+
|
567
|
+
def get(self, name: str) -> P | None:
|
568
|
+
for parameter in self:
|
569
|
+
if parameter.name == name:
|
570
|
+
return parameter
|
571
|
+
return None
|
572
|
+
|
573
|
+
def contains(self, name: str) -> bool:
|
574
|
+
return self.get(name) is not None
|
575
|
+
|
576
|
+
def __contains__(self, item: str) -> bool:
|
577
|
+
return self.contains(item)
|
578
|
+
|
579
|
+
def __bool__(self) -> bool:
|
580
|
+
return bool(self.items)
|
581
|
+
|
582
|
+
def __iter__(self) -> Generator[P, None, None]:
|
583
|
+
yield from iter(self.items)
|
584
|
+
|
585
|
+
def __len__(self) -> int:
|
586
|
+
return len(self.items)
|
587
|
+
|
588
|
+
def __getitem__(self, item: int) -> P:
|
589
|
+
return self.items[item]
|
590
|
+
|
591
|
+
|
592
|
+
class PayloadAlternatives(ParameterSet[P]):
|
593
|
+
"""A set of alternative payloads."""
|
594
|
+
|
595
|
+
|
596
|
+
D = TypeVar("D", bound=dict)
|
597
|
+
|
598
|
+
|
599
|
+
@dataclass(repr=False)
|
600
|
+
class OperationDefinition(Generic[D]):
|
601
|
+
"""A wrapper to store not resolved API operation definitions.
|
602
|
+
|
603
|
+
To prevent recursion errors we need to store definitions without resolving references. But operation definitions
|
604
|
+
itself can be behind a reference (when there is a ``$ref`` in ``paths`` values), therefore we need to store this
|
605
|
+
scope change to have a proper reference resolving later.
|
606
|
+
"""
|
607
|
+
|
608
|
+
raw: D
|
609
|
+
resolved: D
|
610
|
+
scope: str
|
611
|
+
|
612
|
+
__slots__ = ("raw", "resolved", "scope")
|
613
|
+
|
614
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
615
|
+
|
616
|
+
|
617
|
+
@dataclass(eq=False)
|
618
|
+
class APIOperation(Generic[P]):
|
619
|
+
"""An API operation (e.g., `GET /users`)."""
|
620
|
+
|
621
|
+
# `path` does not contain `basePath`
|
622
|
+
# Example <scheme>://<host>/<basePath>/users - "/users" is path
|
623
|
+
# https://swagger.io/docs/specification/2-0/api-host-and-base-path/
|
624
|
+
path: str
|
625
|
+
method: str
|
626
|
+
definition: OperationDefinition = field(repr=False)
|
627
|
+
schema: BaseSchema
|
628
|
+
label: str = None # type: ignore
|
629
|
+
app: Any = None
|
630
|
+
base_url: str | None = None
|
631
|
+
path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
|
632
|
+
headers: ParameterSet[P] = field(default_factory=ParameterSet)
|
633
|
+
cookies: ParameterSet[P] = field(default_factory=ParameterSet)
|
634
|
+
query: ParameterSet[P] = field(default_factory=ParameterSet)
|
635
|
+
body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
|
636
|
+
|
637
|
+
def __post_init__(self) -> None:
|
638
|
+
if self.label is None:
|
639
|
+
self.label = f"{self.method.upper()} {self.path}" # type: ignore
|
640
|
+
|
641
|
+
def __deepcopy__(self, memo: dict) -> APIOperation[P]:
|
642
|
+
return self
|
643
|
+
|
644
|
+
@property
|
645
|
+
def full_path(self) -> str:
|
646
|
+
return self.schema.get_full_path(self.path)
|
647
|
+
|
648
|
+
@property
|
649
|
+
def links(self) -> dict[str, dict[str, Any]]:
|
650
|
+
return self.schema.get_links(self)
|
651
|
+
|
652
|
+
@property
|
653
|
+
def tags(self) -> list[str] | None:
|
654
|
+
return self.schema.get_tags(self)
|
655
|
+
|
656
|
+
def iter_parameters(self) -> Iterator[P]:
|
657
|
+
return chain(self.path_parameters, self.headers, self.cookies, self.query)
|
658
|
+
|
659
|
+
def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
|
660
|
+
return {
|
661
|
+
"path": self.path_parameters,
|
662
|
+
"header": self.headers,
|
663
|
+
"cookie": self.cookies,
|
664
|
+
"query": self.query,
|
665
|
+
"body": self.body,
|
666
|
+
}.get(location)
|
667
|
+
|
668
|
+
def add_parameter(self, parameter: P) -> None:
|
669
|
+
# If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
|
670
|
+
# But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
|
671
|
+
# In this case, we still might generate some tests for an API operation, but without this parameter,
|
672
|
+
# which is better than skip the whole operation from testing.
|
673
|
+
container = self._lookup_container(parameter.location)
|
674
|
+
if container is not None:
|
675
|
+
container.add(parameter)
|
676
|
+
|
677
|
+
def get_parameter(self, name: str, location: str) -> P | None:
|
678
|
+
container = self._lookup_container(location)
|
679
|
+
if container is not None:
|
680
|
+
return container.get(name)
|
681
|
+
return None
|
682
|
+
|
683
|
+
def as_strategy(
|
684
|
+
self,
|
685
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
686
|
+
**kwargs: Any,
|
687
|
+
) -> SearchStrategy[Case]:
|
688
|
+
"""Create a Hypothesis strategy that generates test cases for this API operation.
|
689
|
+
|
690
|
+
Use with `@given` in non-Schemathesis tests.
|
691
|
+
|
692
|
+
Args:
|
693
|
+
generation_mode: Whether to generate positive or negative test data.
|
694
|
+
**kwargs: Extra arguments to the underlying strategy function.
|
695
|
+
|
696
|
+
"""
|
697
|
+
strategy = self.schema.get_case_strategy(self, generation_mode=generation_mode, **kwargs)
|
698
|
+
|
699
|
+
def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
|
700
|
+
context = HookContext(operation=self)
|
701
|
+
for hook in dispatcher.get_all_by_name("before_generate_case"):
|
702
|
+
_strategy = hook(context, _strategy)
|
703
|
+
for hook in dispatcher.get_all_by_name("filter_case"):
|
704
|
+
hook = partial(hook, context)
|
705
|
+
_strategy = _strategy.filter(hook)
|
706
|
+
for hook in dispatcher.get_all_by_name("map_case"):
|
707
|
+
hook = partial(hook, context)
|
708
|
+
_strategy = _strategy.map(hook)
|
709
|
+
for hook in dispatcher.get_all_by_name("flatmap_case"):
|
710
|
+
hook = partial(hook, context)
|
711
|
+
_strategy = _strategy.flatmap(hook)
|
712
|
+
return _strategy
|
713
|
+
|
714
|
+
strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
|
715
|
+
strategy = _apply_hooks(self.schema.hooks, strategy)
|
716
|
+
hooks = kwargs.get("hooks")
|
717
|
+
if hooks is not None:
|
718
|
+
strategy = _apply_hooks(hooks, strategy)
|
719
|
+
return strategy
|
720
|
+
|
721
|
+
def get_security_requirements(self) -> list[str]:
|
722
|
+
return self.schema.get_security_requirements(self)
|
723
|
+
|
724
|
+
def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
725
|
+
return self.schema.get_strategies_from_examples(self, **kwargs)
|
726
|
+
|
727
|
+
def get_parameter_serializer(self, location: str) -> Callable | None:
|
728
|
+
return self.schema.get_parameter_serializer(self, location)
|
729
|
+
|
730
|
+
def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
|
731
|
+
return self.schema.prepare_multipart(form_data, self)
|
732
|
+
|
733
|
+
def get_request_payload_content_types(self) -> list[str]:
|
734
|
+
return self.schema.get_request_payload_content_types(self)
|
735
|
+
|
736
|
+
def _get_default_media_type(self) -> str:
|
737
|
+
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
738
|
+
media_types = self.get_request_payload_content_types()
|
739
|
+
if len(media_types) == 1:
|
740
|
+
# The only available option
|
741
|
+
return media_types[0]
|
742
|
+
media_types_repr = ", ".join(media_types)
|
743
|
+
raise IncorrectUsage(
|
744
|
+
"Can not detect appropriate media type. "
|
745
|
+
"You can either specify one of the defined media types "
|
746
|
+
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
747
|
+
)
|
748
|
+
|
749
|
+
def Case(
|
750
|
+
self,
|
751
|
+
*,
|
752
|
+
method: str | None = None,
|
753
|
+
path_parameters: dict[str, Any] | None = None,
|
754
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
755
|
+
cookies: dict[str, Any] | None = None,
|
756
|
+
query: dict[str, Any] | None = None,
|
757
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
758
|
+
media_type: str | None = None,
|
759
|
+
_meta: CaseMetadata | None = None,
|
760
|
+
) -> Case:
|
761
|
+
"""Create a test case with specific data instead of generated values.
|
762
|
+
|
763
|
+
Args:
|
764
|
+
method: Override HTTP method.
|
765
|
+
path_parameters: Override path variables.
|
766
|
+
headers: Override HTTP headers.
|
767
|
+
cookies: Override cookies.
|
768
|
+
query: Override query parameters.
|
769
|
+
body: Override request body.
|
770
|
+
media_type: Override media type.
|
771
|
+
|
772
|
+
"""
|
773
|
+
from requests.structures import CaseInsensitiveDict
|
774
|
+
|
775
|
+
return self.schema.make_case(
|
776
|
+
operation=self,
|
777
|
+
method=method,
|
778
|
+
path_parameters=path_parameters or {},
|
779
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
780
|
+
cookies=cookies or {},
|
781
|
+
query=query or {},
|
782
|
+
body=body,
|
783
|
+
media_type=media_type,
|
784
|
+
meta=_meta,
|
785
|
+
)
|
786
|
+
|
787
|
+
@property
|
788
|
+
def operation_reference(self) -> str:
|
789
|
+
path = self.path.replace("~", "~0").replace("/", "~1")
|
790
|
+
return f"#/paths/{path}/{self.method}"
|
791
|
+
|
792
|
+
def validate_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool | None:
|
793
|
+
"""Validate a response against the API schema.
|
794
|
+
|
795
|
+
Args:
|
796
|
+
response: The HTTP response to validate. Can be a `requests.Response`,
|
797
|
+
`httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
|
798
|
+
|
799
|
+
Raises:
|
800
|
+
FailureGroup: If the response does not conform to the schema.
|
801
|
+
|
802
|
+
"""
|
803
|
+
return self.schema.validate_response(self, Response.from_any(response))
|
804
|
+
|
805
|
+
def is_valid_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool:
|
806
|
+
"""Check if the provided response is valid against the API schema.
|
807
|
+
|
808
|
+
Args:
|
809
|
+
response: The HTTP response to validate. Can be a `requests.Response`,
|
810
|
+
`httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
|
811
|
+
|
812
|
+
Returns:
|
813
|
+
`True` if response is valid, `False` otherwise.
|
814
|
+
|
815
|
+
"""
|
816
|
+
try:
|
817
|
+
self.validate_response(response)
|
818
|
+
return True
|
819
|
+
except AssertionError:
|
820
|
+
return False
|
821
|
+
|
822
|
+
def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
823
|
+
return self.schema._get_payload_schema(self.definition.raw, media_type)
|
824
|
+
|
825
|
+
def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
826
|
+
return self.schema._get_payload_schema(self.definition.resolved, media_type)
|