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