schemathesis 3.25.5__py3-none-any.whl → 3.39.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
from ipaddress import IPv4Network, IPv6Network
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
7
|
+
|
|
8
|
+
from ..graphql import nodes
|
|
9
|
+
from ..internal.result import Err, Ok, Result
|
|
10
|
+
from .models import (
|
|
11
|
+
Extension,
|
|
12
|
+
GraphQLScalarsExtension,
|
|
13
|
+
MediaTypesExtension,
|
|
14
|
+
OpenApiStringFormatsExtension,
|
|
15
|
+
SchemaPatchesExtension,
|
|
16
|
+
StrategyDefinition,
|
|
17
|
+
TransformFunctionDefinition,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from datetime import date, datetime
|
|
22
|
+
|
|
23
|
+
from hypothesis import strategies as st
|
|
24
|
+
|
|
25
|
+
from ..schemas import BaseSchema
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def apply(extensions: list[Extension], schema: BaseSchema) -> None:
|
|
29
|
+
"""Apply the given extensions."""
|
|
30
|
+
for extension in extensions:
|
|
31
|
+
if isinstance(extension, OpenApiStringFormatsExtension):
|
|
32
|
+
_apply_string_formats_extension(extension)
|
|
33
|
+
elif isinstance(extension, GraphQLScalarsExtension):
|
|
34
|
+
_apply_scalars_extension(extension)
|
|
35
|
+
elif isinstance(extension, MediaTypesExtension):
|
|
36
|
+
_apply_media_types_extension(extension)
|
|
37
|
+
elif isinstance(extension, SchemaPatchesExtension):
|
|
38
|
+
_apply_schema_patches_extension(extension, schema)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _apply_simple_extension(
|
|
42
|
+
extension: OpenApiStringFormatsExtension | GraphQLScalarsExtension | MediaTypesExtension,
|
|
43
|
+
collection: dict[str, Any],
|
|
44
|
+
register_strategy: Callable[[str, st.SearchStrategy], None],
|
|
45
|
+
) -> None:
|
|
46
|
+
errors = []
|
|
47
|
+
for name, value in collection.items():
|
|
48
|
+
strategy = strategy_from_definitions(value)
|
|
49
|
+
if isinstance(strategy, Err):
|
|
50
|
+
errors.append(str(strategy.err()))
|
|
51
|
+
else:
|
|
52
|
+
register_strategy(name, strategy.ok())
|
|
53
|
+
|
|
54
|
+
if errors:
|
|
55
|
+
extension.set_error(errors=errors)
|
|
56
|
+
else:
|
|
57
|
+
extension.set_success()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _apply_string_formats_extension(extension: OpenApiStringFormatsExtension) -> None:
|
|
61
|
+
from ..specs.openapi import formats
|
|
62
|
+
|
|
63
|
+
_apply_simple_extension(extension, extension.formats, formats.register)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _apply_scalars_extension(extension: GraphQLScalarsExtension) -> None:
|
|
67
|
+
from ..specs.graphql import scalars
|
|
68
|
+
|
|
69
|
+
_apply_simple_extension(extension, extension.scalars, scalars.scalar)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _apply_media_types_extension(extension: MediaTypesExtension) -> None:
|
|
73
|
+
from ..specs.openapi import media_types
|
|
74
|
+
|
|
75
|
+
_apply_simple_extension(extension, extension.media_types, media_types.register_media_type)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _find_built_in_strategy(name: str) -> st.SearchStrategy | None:
|
|
79
|
+
"""Find a built-in Hypothesis strategy by its name."""
|
|
80
|
+
from hypothesis import provisional as pr
|
|
81
|
+
from hypothesis import strategies as st
|
|
82
|
+
|
|
83
|
+
for module in (st, pr):
|
|
84
|
+
if hasattr(module, name):
|
|
85
|
+
return getattr(module, name)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: BaseSchema) -> None:
|
|
90
|
+
"""Apply a set of patches to the schema."""
|
|
91
|
+
for patch in extension.patches:
|
|
92
|
+
current: dict[str, Any] | list = schema.raw_schema
|
|
93
|
+
operation = patch["operation"]
|
|
94
|
+
path = patch["path"]
|
|
95
|
+
for part in path[:-1]:
|
|
96
|
+
if isinstance(current, dict):
|
|
97
|
+
if not isinstance(part, str):
|
|
98
|
+
extension.set_error([f"Invalid path: {path}"])
|
|
99
|
+
return
|
|
100
|
+
current = current.setdefault(part, {})
|
|
101
|
+
elif isinstance(current, list):
|
|
102
|
+
if not isinstance(part, int):
|
|
103
|
+
extension.set_error([f"Invalid path: {path}"])
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
current = current[part]
|
|
107
|
+
except IndexError:
|
|
108
|
+
extension.set_error([f"Invalid path: {path}"])
|
|
109
|
+
return
|
|
110
|
+
if operation == "add":
|
|
111
|
+
# Add or replace the value at the target location.
|
|
112
|
+
current[path[-1]] = patch["value"] # type: ignore
|
|
113
|
+
elif operation == "remove":
|
|
114
|
+
# Remove the item at the target location if it exists.
|
|
115
|
+
if path:
|
|
116
|
+
last = path[-1]
|
|
117
|
+
if isinstance(current, dict) and isinstance(last, str) and last in current:
|
|
118
|
+
del current[last]
|
|
119
|
+
elif isinstance(current, list) and isinstance(last, int) and len(current) > last:
|
|
120
|
+
del current[last]
|
|
121
|
+
else:
|
|
122
|
+
extension.set_error([f"Invalid path: {path}"])
|
|
123
|
+
return
|
|
124
|
+
else:
|
|
125
|
+
current.clear()
|
|
126
|
+
|
|
127
|
+
extension.set_success()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
|
|
131
|
+
from ..generation import combine_strategies
|
|
132
|
+
|
|
133
|
+
strategies = []
|
|
134
|
+
for definition in definitions:
|
|
135
|
+
strategy = _strategy_from_definition(definition)
|
|
136
|
+
if isinstance(strategy, Ok):
|
|
137
|
+
strategies.append(strategy.ok())
|
|
138
|
+
else:
|
|
139
|
+
return strategy
|
|
140
|
+
return Ok(combine_strategies(strategies))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
KNOWN_ARGUMENTS = {
|
|
144
|
+
"IPv4Network": IPv4Network,
|
|
145
|
+
"IPv6Network": IPv6Network,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def check_regex(regex: str) -> Result[None, Exception]:
|
|
150
|
+
try:
|
|
151
|
+
re.compile(regex)
|
|
152
|
+
except (re.error, OverflowError, RuntimeError):
|
|
153
|
+
return Err(ValueError(f"Invalid regex: `{regex}`"))
|
|
154
|
+
return Ok(None)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def check_sampled_from(elements: list) -> Result[None, Exception]:
|
|
158
|
+
if not elements:
|
|
159
|
+
return Err(ValueError("Invalid input for `sampled_from`: Cannot sample from a length-zero sequence"))
|
|
160
|
+
return Ok(None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
STRATEGY_ARGUMENT_CHECKS = {
|
|
164
|
+
"from_regex": check_regex,
|
|
165
|
+
"sampled_from": check_sampled_from,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _strategy_from_definition(definition: StrategyDefinition) -> Result[st.SearchStrategy, Exception]:
|
|
170
|
+
base = _find_built_in_strategy(definition.name)
|
|
171
|
+
if base is None:
|
|
172
|
+
return Err(ValueError(f"Unknown built-in strategy: `{definition.name}`"))
|
|
173
|
+
arguments = definition.arguments or {}
|
|
174
|
+
arguments = arguments.copy()
|
|
175
|
+
for key, value in arguments.items():
|
|
176
|
+
if isinstance(value, str):
|
|
177
|
+
known = KNOWN_ARGUMENTS.get(value)
|
|
178
|
+
if known is not None:
|
|
179
|
+
arguments[key] = known
|
|
180
|
+
check = STRATEGY_ARGUMENT_CHECKS.get(definition.name)
|
|
181
|
+
if check is not None:
|
|
182
|
+
check_result = check(**arguments) # type: ignore
|
|
183
|
+
if isinstance(check_result, Err):
|
|
184
|
+
return check_result
|
|
185
|
+
strategy = base(**arguments)
|
|
186
|
+
for transform in definition.transforms or []:
|
|
187
|
+
if transform["kind"] == "map":
|
|
188
|
+
function = _get_map_function(transform)
|
|
189
|
+
if isinstance(function, Ok):
|
|
190
|
+
strategy = strategy.map(function.ok())
|
|
191
|
+
else:
|
|
192
|
+
return function
|
|
193
|
+
else:
|
|
194
|
+
return Err(ValueError(f"Unknown transform kind: {transform['kind']}"))
|
|
195
|
+
|
|
196
|
+
return Ok(strategy)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def make_strftime(format: str) -> Callable:
|
|
200
|
+
def strftime(value: date | datetime) -> str:
|
|
201
|
+
return value.strftime(format)
|
|
202
|
+
|
|
203
|
+
return strftime
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_map_function(definition: TransformFunctionDefinition) -> Result[Callable | None, Exception]:
|
|
207
|
+
from ..serializers import Binary
|
|
208
|
+
|
|
209
|
+
TRANSFORM_FACTORIES: dict[str, Callable] = {
|
|
210
|
+
"str": lambda: str,
|
|
211
|
+
"base64_encode": lambda: lambda x: Binary(base64.b64encode(x)),
|
|
212
|
+
"base64_decode": lambda: lambda x: Binary(base64.b64decode(x)),
|
|
213
|
+
"urlsafe_base64_encode": lambda: lambda x: Binary(base64.urlsafe_b64encode(x)),
|
|
214
|
+
"strftime": make_strftime,
|
|
215
|
+
"GraphQLBoolean": lambda: nodes.Boolean,
|
|
216
|
+
"GraphQLFloat": lambda: nodes.Float,
|
|
217
|
+
"GraphQLInt": lambda: nodes.Int,
|
|
218
|
+
"GraphQLString": lambda: nodes.String,
|
|
219
|
+
}
|
|
220
|
+
factory = TRANSFORM_FACTORIES.get(definition["name"])
|
|
221
|
+
if factory is None:
|
|
222
|
+
return Err(ValueError(f"Unknown transform: {definition['name']}"))
|
|
223
|
+
arguments = definition.get("arguments", {})
|
|
224
|
+
return Ok(factory(**arguments))
|
schemathesis/service/hosts.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
"""Work with stored auth data."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import enum
|
|
4
6
|
import tempfile
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
8
10
|
|
|
9
11
|
import tomli
|
|
10
12
|
import tomli_w
|
|
11
13
|
|
|
12
|
-
from ..types import PathLike
|
|
13
14
|
from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..types import PathLike
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
@dataclass
|
|
17
21
|
class HostData:
|
schemathesis/service/metadata.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Useful info to collect from CLI usage."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import os
|
|
4
6
|
import platform
|
|
5
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from importlib import metadata
|
|
6
9
|
|
|
7
10
|
from ..constants import SCHEMATHESIS_VERSION
|
|
8
11
|
from .constants import DOCKER_IMAGE_ENV_VAR
|
|
@@ -32,6 +35,27 @@ class CliMetadata:
|
|
|
32
35
|
version: str = SCHEMATHESIS_VERSION
|
|
33
36
|
|
|
34
37
|
|
|
38
|
+
DEPENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Dependency:
|
|
43
|
+
"""A single dependency."""
|
|
44
|
+
|
|
45
|
+
# Name of the package.
|
|
46
|
+
name: str
|
|
47
|
+
# Version of the package.
|
|
48
|
+
version: str
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_name(cls, name: str) -> Dependency:
|
|
52
|
+
return cls(name=name, version=metadata.version(name))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def collect_dependency_versions() -> list[Dependency]:
|
|
56
|
+
return [Dependency.from_name(name) for name in DEPENDENCY_NAMES]
|
|
57
|
+
|
|
58
|
+
|
|
35
59
|
@dataclass
|
|
36
60
|
class Metadata:
|
|
37
61
|
"""CLI environment metadata."""
|
|
@@ -44,3 +68,4 @@ class Metadata:
|
|
|
44
68
|
cli: CliMetadata = field(default_factory=CliMetadata)
|
|
45
69
|
# Used Docker image if any
|
|
46
70
|
docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
|
|
71
|
+
depedenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
|
schemathesis/service/models.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
3
4
|
from enum import Enum
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any, Iterable, Literal, TypedDict, Union
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class UploadSource(str, Enum):
|
|
@@ -47,3 +48,211 @@ class UploadResponse:
|
|
|
47
48
|
@dataclass
|
|
48
49
|
class FailedUploadResponse:
|
|
49
50
|
detail: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class NotAppliedState:
|
|
55
|
+
"""The extension was not applied."""
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return "Not Applied"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SuccessState:
|
|
63
|
+
"""The extension was applied successfully."""
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return "Success"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ErrorState:
|
|
71
|
+
"""An error occurred during the extension application."""
|
|
72
|
+
|
|
73
|
+
errors: list[str] = field(default_factory=list)
|
|
74
|
+
exceptions: list[Exception] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return "Error"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
ExtensionState = Union[NotAppliedState, SuccessState, ErrorState]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class BaseExtension:
|
|
85
|
+
def set_state(self, state: ExtensionState) -> None:
|
|
86
|
+
self.state = state
|
|
87
|
+
|
|
88
|
+
def set_success(self) -> None:
|
|
89
|
+
self.set_state(SuccessState())
|
|
90
|
+
|
|
91
|
+
def set_error(self, errors: list[str] | None = None, exceptions: list[Exception] | None = None) -> None:
|
|
92
|
+
self.set_state(ErrorState(errors=errors or [], exceptions=exceptions or []))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class UnknownExtension(BaseExtension):
|
|
97
|
+
"""An unknown extension.
|
|
98
|
+
|
|
99
|
+
Likely the CLI should be updated.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
type: str
|
|
103
|
+
state: ExtensionState = field(default_factory=NotAppliedState)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def summary(self) -> str:
|
|
107
|
+
return f"`{self.type}`"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AddPatch(TypedDict):
|
|
111
|
+
operation: Literal["add"]
|
|
112
|
+
path: list[str | int]
|
|
113
|
+
value: Any
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RemovePatch(TypedDict):
|
|
117
|
+
operation: Literal["remove"]
|
|
118
|
+
path: list[str | int]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
Patch = Union[AddPatch, RemovePatch]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class SchemaPatchesExtension(BaseExtension):
|
|
126
|
+
"""Update the schema with its optimized version."""
|
|
127
|
+
|
|
128
|
+
patches: list[Patch]
|
|
129
|
+
state: ExtensionState = field(default_factory=NotAppliedState)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def summary(self) -> str:
|
|
133
|
+
count = len(self.patches)
|
|
134
|
+
plural = "es" if count > 1 else ""
|
|
135
|
+
return f"{count} schema patch{plural}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TransformFunctionDefinition(TypedDict):
|
|
139
|
+
kind: Literal["map", "filter"]
|
|
140
|
+
name: str
|
|
141
|
+
arguments: dict[str, Any]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class StrategyDefinition:
|
|
146
|
+
name: str
|
|
147
|
+
transforms: list[TransformFunctionDefinition] | None = None
|
|
148
|
+
arguments: dict[str, Any] | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _strategies_from_definition(items: dict[str, list[dict[str, Any]]]) -> dict[str, list[StrategyDefinition]]:
|
|
152
|
+
return {name: [StrategyDefinition(**item) for item in value] for name, value in items.items()}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _format_items(items: Iterable[str]) -> str:
|
|
156
|
+
return ", ".join([f"`{item}`" for item in items])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class OpenApiStringFormatsExtension(BaseExtension):
|
|
161
|
+
"""Custom string formats."""
|
|
162
|
+
|
|
163
|
+
formats: dict[str, list[StrategyDefinition]]
|
|
164
|
+
state: ExtensionState = field(default_factory=NotAppliedState)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_dict(cls, formats: dict[str, list[dict[str, Any]]]) -> OpenApiStringFormatsExtension:
|
|
168
|
+
return cls(formats=_strategies_from_definition(formats))
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def summary(self) -> str:
|
|
172
|
+
count = len(self.formats)
|
|
173
|
+
plural = "s" if count > 1 else ""
|
|
174
|
+
formats = _format_items(self.formats)
|
|
175
|
+
return f"Data generator{plural} for {formats} Open API format{plural}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class GraphQLScalarsExtension(BaseExtension):
|
|
180
|
+
"""Custom scalars."""
|
|
181
|
+
|
|
182
|
+
scalars: dict[str, list[StrategyDefinition]]
|
|
183
|
+
state: ExtensionState = field(default_factory=NotAppliedState)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, scalars: dict[str, list[dict[str, Any]]]) -> GraphQLScalarsExtension:
|
|
187
|
+
return cls(scalars=_strategies_from_definition(scalars))
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def summary(self) -> str:
|
|
191
|
+
count = len(self.scalars)
|
|
192
|
+
plural = "s" if count > 1 else ""
|
|
193
|
+
scalars = _format_items(self.scalars)
|
|
194
|
+
return f"Data generator{plural} for {scalars} GraphQL scalar{plural}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class MediaTypesExtension(BaseExtension):
|
|
199
|
+
media_types: dict[str, list[StrategyDefinition]]
|
|
200
|
+
state: ExtensionState = field(default_factory=NotAppliedState)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def from_dict(cls, media_types: dict[str, list[dict[str, Any]]]) -> MediaTypesExtension:
|
|
204
|
+
return cls(media_types=_strategies_from_definition(media_types))
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def summary(self) -> str:
|
|
208
|
+
count = len(self.media_types)
|
|
209
|
+
plural = "s" if count > 1 else ""
|
|
210
|
+
media_types = _format_items(self.media_types)
|
|
211
|
+
return f"Data generator{plural} for {media_types} media type{plural}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# A CLI extension that can be used to adjust the behavior of Schemathesis.
|
|
215
|
+
Extension = Union[
|
|
216
|
+
SchemaPatchesExtension,
|
|
217
|
+
OpenApiStringFormatsExtension,
|
|
218
|
+
GraphQLScalarsExtension,
|
|
219
|
+
MediaTypesExtension,
|
|
220
|
+
UnknownExtension,
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def extension_from_dict(data: dict[str, Any]) -> Extension:
|
|
225
|
+
if data["type"] == "schema_patches":
|
|
226
|
+
return SchemaPatchesExtension(patches=data["patches"])
|
|
227
|
+
if data["type"] == "string_formats":
|
|
228
|
+
return OpenApiStringFormatsExtension.from_dict(formats=data["items"])
|
|
229
|
+
if data["type"] == "scalars":
|
|
230
|
+
return GraphQLScalarsExtension.from_dict(scalars=data["items"])
|
|
231
|
+
if data["type"] == "media_types":
|
|
232
|
+
return MediaTypesExtension.from_dict(media_types=data["items"])
|
|
233
|
+
return UnknownExtension(type=data["type"])
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class AnalysisSuccess:
|
|
238
|
+
id: str
|
|
239
|
+
elapsed: float
|
|
240
|
+
message: str
|
|
241
|
+
extensions: list[Extension]
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def from_dict(cls, data: dict[str, Any]) -> AnalysisSuccess:
|
|
245
|
+
return cls(
|
|
246
|
+
id=data["id"],
|
|
247
|
+
elapsed=data["elapsed"],
|
|
248
|
+
message=data["message"],
|
|
249
|
+
extensions=[extension_from_dict(ext) for ext in data["extensions"]],
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class AnalysisError:
|
|
255
|
+
message: str
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
AnalysisResult = Union[AnalysisSuccess, AnalysisError]
|
schemathesis/service/report.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
@@ -9,24 +10,23 @@ from contextlib import suppress
|
|
|
9
10
|
from dataclasses import asdict, dataclass, field
|
|
10
11
|
from io import BytesIO
|
|
11
12
|
from queue import Queue
|
|
12
|
-
from typing import
|
|
13
|
-
|
|
14
|
-
import click
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
15
14
|
|
|
16
15
|
from ..cli.handlers import EventHandler
|
|
17
16
|
from ..runner.events import Initialized, InternalError, Interrupted
|
|
18
17
|
from . import ci, events, usage
|
|
19
18
|
from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
|
|
20
|
-
from .hosts import HostData
|
|
21
19
|
from .metadata import Metadata
|
|
22
20
|
from .models import UploadResponse
|
|
23
21
|
from .serialization import serialize_event
|
|
24
22
|
|
|
25
|
-
|
|
26
23
|
if TYPE_CHECKING:
|
|
27
|
-
|
|
24
|
+
import click
|
|
25
|
+
|
|
28
26
|
from ..cli.context import ExecutionContext
|
|
29
27
|
from ..runner.events import ExecutionEvent
|
|
28
|
+
from .client import ServiceClient
|
|
29
|
+
from .hosts import HostData
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|