schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
schemathesis/service/hosts.py
DELETED
@@ -1,111 +0,0 @@
|
|
1
|
-
"""Work with stored auth data."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import enum
|
6
|
-
import tempfile
|
7
|
-
from dataclasses import dataclass
|
8
|
-
from pathlib import Path
|
9
|
-
from typing import TYPE_CHECKING, Any
|
10
|
-
|
11
|
-
import tomli
|
12
|
-
import tomli_w
|
13
|
-
|
14
|
-
from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
|
15
|
-
|
16
|
-
if TYPE_CHECKING:
|
17
|
-
from ..types import PathLike
|
18
|
-
|
19
|
-
|
20
|
-
@dataclass
|
21
|
-
class HostData:
|
22
|
-
"""Stored data related to a host."""
|
23
|
-
|
24
|
-
hostname: str
|
25
|
-
hosts_file: PathLike
|
26
|
-
|
27
|
-
def load(self) -> dict[str, Any]:
|
28
|
-
return load(self.hosts_file).get(self.hostname, {})
|
29
|
-
|
30
|
-
@property
|
31
|
-
def correlation_id(self) -> str | None:
|
32
|
-
return self.load().get("correlation_id")
|
33
|
-
|
34
|
-
def store_correlation_id(self, correlation_id: str) -> None:
|
35
|
-
"""Store `correlation_id` in the hosts file."""
|
36
|
-
hosts = load(self.hosts_file)
|
37
|
-
data = hosts.setdefault(self.hostname, {})
|
38
|
-
data["correlation_id"] = correlation_id
|
39
|
-
_dump_hosts(self.hosts_file, hosts)
|
40
|
-
|
41
|
-
|
42
|
-
def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> None:
|
43
|
-
"""Store a new token for a host."""
|
44
|
-
# Don't use any file-based locking for simplicity
|
45
|
-
hosts = load(hosts_file)
|
46
|
-
data = hosts.setdefault(hostname, {})
|
47
|
-
data.update(version=HOSTS_FORMAT_VERSION, token=token)
|
48
|
-
_dump_hosts(hosts_file, hosts)
|
49
|
-
|
50
|
-
|
51
|
-
def load(path: PathLike) -> dict[str, Any]:
|
52
|
-
"""Load the given hosts file.
|
53
|
-
|
54
|
-
Return an empty dict if it doesn't exist.
|
55
|
-
"""
|
56
|
-
from ..utils import _ensure_parent
|
57
|
-
|
58
|
-
try:
|
59
|
-
with open(path, "rb") as fd:
|
60
|
-
return tomli.load(fd)
|
61
|
-
except FileNotFoundError:
|
62
|
-
_ensure_parent(path)
|
63
|
-
return {}
|
64
|
-
except tomli.TOMLDecodeError:
|
65
|
-
return {}
|
66
|
-
|
67
|
-
|
68
|
-
def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
|
69
|
-
"""Load all data associated with a hostname."""
|
70
|
-
return load(hosts_file).get(hostname, {})
|
71
|
-
|
72
|
-
|
73
|
-
@enum.unique
|
74
|
-
class RemoveAuth(enum.Enum):
|
75
|
-
success = 1
|
76
|
-
no_match = 2
|
77
|
-
no_hosts = 3
|
78
|
-
error = 4
|
79
|
-
|
80
|
-
|
81
|
-
def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> RemoveAuth:
|
82
|
-
"""Remove authentication for a Schemathesis.io host."""
|
83
|
-
try:
|
84
|
-
with open(hosts_file, "rb") as fd:
|
85
|
-
hosts = tomli.load(fd)
|
86
|
-
try:
|
87
|
-
hosts.pop(hostname)
|
88
|
-
_dump_hosts(hosts_file, hosts)
|
89
|
-
return RemoveAuth.success
|
90
|
-
except KeyError:
|
91
|
-
return RemoveAuth.no_match
|
92
|
-
except FileNotFoundError:
|
93
|
-
return RemoveAuth.no_hosts
|
94
|
-
except tomli.TOMLDecodeError:
|
95
|
-
return RemoveAuth.error
|
96
|
-
|
97
|
-
|
98
|
-
def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
|
99
|
-
"""Load a token for a host."""
|
100
|
-
return load_for_host(hostname, hosts_file).get("token")
|
101
|
-
|
102
|
-
|
103
|
-
def get_temporary_hosts_file() -> str:
|
104
|
-
temporary_dir = Path(tempfile.gettempdir()).resolve()
|
105
|
-
return str(temporary_dir / "schemathesis-hosts.toml")
|
106
|
-
|
107
|
-
|
108
|
-
def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
|
109
|
-
"""Write hosts data to a file."""
|
110
|
-
with open(path, "wb") as fd:
|
111
|
-
tomli_w.dump(hosts, fd)
|
schemathesis/service/metadata.py
DELETED
@@ -1,71 +0,0 @@
|
|
1
|
-
"""Useful info to collect from CLI usage."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import os
|
6
|
-
import platform
|
7
|
-
from dataclasses import dataclass, field
|
8
|
-
from importlib import metadata
|
9
|
-
|
10
|
-
from ..constants import SCHEMATHESIS_VERSION
|
11
|
-
from .constants import DOCKER_IMAGE_ENV_VAR
|
12
|
-
|
13
|
-
|
14
|
-
@dataclass
|
15
|
-
class PlatformMetadata:
|
16
|
-
# System / OS name, e.g. "Linux" or "Windows".
|
17
|
-
system: str = field(default_factory=platform.system)
|
18
|
-
# System release, e.g. "5.14" or "NT".
|
19
|
-
release: str = field(default_factory=platform.release)
|
20
|
-
# Machine type, e.g. "i386".
|
21
|
-
machine: str = field(default_factory=platform.machine)
|
22
|
-
|
23
|
-
|
24
|
-
@dataclass
|
25
|
-
class InterpreterMetadata:
|
26
|
-
# The Python version as "major.minor.patch".
|
27
|
-
version: str = field(default_factory=platform.python_version)
|
28
|
-
# Python implementation, e.g. "CPython" or "PyPy".
|
29
|
-
implementation: str = field(default_factory=platform.python_implementation)
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass
|
33
|
-
class CliMetadata:
|
34
|
-
# Schemathesis package version.
|
35
|
-
version: str = SCHEMATHESIS_VERSION
|
36
|
-
|
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
|
-
|
59
|
-
@dataclass
|
60
|
-
class Metadata:
|
61
|
-
"""CLI environment metadata."""
|
62
|
-
|
63
|
-
# Information about the host platform.
|
64
|
-
platform: PlatformMetadata = field(default_factory=PlatformMetadata)
|
65
|
-
# Python interpreter info.
|
66
|
-
interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
|
67
|
-
# CLI info itself.
|
68
|
-
cli: CliMetadata = field(default_factory=CliMetadata)
|
69
|
-
# Used Docker image if any
|
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
DELETED
@@ -1,258 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from enum import Enum
|
5
|
-
from typing import Any, Iterable, Literal, TypedDict, Union
|
6
|
-
|
7
|
-
|
8
|
-
class UploadSource(str, Enum):
|
9
|
-
DEFAULT = "default"
|
10
|
-
UPLOAD_COMMAND = "upload_command"
|
11
|
-
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class ProjectDetails:
|
15
|
-
environments: list[ProjectEnvironment]
|
16
|
-
specification: Specification
|
17
|
-
|
18
|
-
@property
|
19
|
-
def default_environment(self) -> ProjectEnvironment | None:
|
20
|
-
return next((env for env in self.environments if env.is_default), None)
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class ProjectEnvironment:
|
25
|
-
url: str
|
26
|
-
name: str
|
27
|
-
description: str
|
28
|
-
is_default: bool
|
29
|
-
|
30
|
-
|
31
|
-
@dataclass
|
32
|
-
class Specification:
|
33
|
-
schema: dict[str, Any]
|
34
|
-
|
35
|
-
|
36
|
-
@dataclass
|
37
|
-
class AuthResponse:
|
38
|
-
username: str
|
39
|
-
|
40
|
-
|
41
|
-
@dataclass
|
42
|
-
class UploadResponse:
|
43
|
-
message: str
|
44
|
-
next_url: str
|
45
|
-
correlation_id: str
|
46
|
-
|
47
|
-
|
48
|
-
@dataclass
|
49
|
-
class FailedUploadResponse:
|
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
DELETED
@@ -1,255 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import enum
|
4
|
-
import json
|
5
|
-
import os
|
6
|
-
import tarfile
|
7
|
-
import threading
|
8
|
-
import time
|
9
|
-
from contextlib import suppress
|
10
|
-
from dataclasses import asdict, dataclass, field
|
11
|
-
from io import BytesIO
|
12
|
-
from queue import Queue
|
13
|
-
from typing import TYPE_CHECKING, Any
|
14
|
-
|
15
|
-
from ..cli.handlers import EventHandler
|
16
|
-
from ..runner.events import Initialized, InternalError, Interrupted
|
17
|
-
from . import ci, events, usage
|
18
|
-
from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
|
19
|
-
from .metadata import Metadata
|
20
|
-
from .models import UploadResponse
|
21
|
-
from .serialization import serialize_event
|
22
|
-
|
23
|
-
if TYPE_CHECKING:
|
24
|
-
import click
|
25
|
-
|
26
|
-
from ..cli.context import ExecutionContext
|
27
|
-
from ..runner.events import ExecutionEvent
|
28
|
-
from .client import ServiceClient
|
29
|
-
from .hosts import HostData
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass
|
33
|
-
class ReportWriter:
|
34
|
-
"""Schemathesis.io test run report.
|
35
|
-
|
36
|
-
Simplifies adding new files to the archive.
|
37
|
-
"""
|
38
|
-
|
39
|
-
_tar: tarfile.TarFile
|
40
|
-
_events_count: int = 0
|
41
|
-
|
42
|
-
def add_json_file(self, name: str, data: Any) -> None:
|
43
|
-
buffer = BytesIO()
|
44
|
-
buffer.write(json.dumps(data, separators=(",", ":")).encode())
|
45
|
-
buffer.seek(0)
|
46
|
-
info = tarfile.TarInfo(name=name)
|
47
|
-
info.size = len(buffer.getbuffer())
|
48
|
-
info.mtime = int(time.time())
|
49
|
-
self._tar.addfile(info, buffer)
|
50
|
-
|
51
|
-
def add_metadata(
|
52
|
-
self,
|
53
|
-
*,
|
54
|
-
api_name: str | None,
|
55
|
-
location: str,
|
56
|
-
base_url: str | None,
|
57
|
-
started_at: str,
|
58
|
-
metadata: Metadata,
|
59
|
-
ci_environment: ci.Environment | None,
|
60
|
-
usage_data: dict[str, Any] | None,
|
61
|
-
) -> None:
|
62
|
-
data = {
|
63
|
-
# API identifier on the Schemathesis.io side (optional)
|
64
|
-
"api_name": api_name,
|
65
|
-
# The place, where the API schema is located
|
66
|
-
"location": location,
|
67
|
-
# The base URL against which the tests are running
|
68
|
-
"base_url": base_url,
|
69
|
-
# The time that the test run began
|
70
|
-
"started_at": started_at,
|
71
|
-
# Metadata about CLI environment
|
72
|
-
"environment": asdict(metadata),
|
73
|
-
# Environment variables specific for CI providers
|
74
|
-
"ci": ci_environment.asdict() if ci_environment is not None else None,
|
75
|
-
# CLI usage statistic
|
76
|
-
"usage": usage_data,
|
77
|
-
# Report format version
|
78
|
-
"version": REPORT_FORMAT_VERSION,
|
79
|
-
}
|
80
|
-
self.add_json_file("metadata.json", data)
|
81
|
-
|
82
|
-
def add_event(self, event: ExecutionEvent) -> None:
|
83
|
-
"""Add an execution event to the report."""
|
84
|
-
self._events_count += 1
|
85
|
-
filename = f"events/{self._events_count}-{event.__class__.__name__}.json"
|
86
|
-
self.add_json_file(filename, serialize_event(event))
|
87
|
-
|
88
|
-
|
89
|
-
class BaseReportHandler(EventHandler):
|
90
|
-
in_queue: Queue
|
91
|
-
worker: threading.Thread
|
92
|
-
|
93
|
-
def handle_event(self, context: ExecutionContext, event: ExecutionEvent) -> None:
|
94
|
-
self.in_queue.put(event)
|
95
|
-
|
96
|
-
def shutdown(self) -> None:
|
97
|
-
self._stop_worker()
|
98
|
-
|
99
|
-
def _stop_worker(self) -> None:
|
100
|
-
self.in_queue.put(STOP_MARKER)
|
101
|
-
self.worker.join(WORKER_JOIN_TIMEOUT)
|
102
|
-
|
103
|
-
|
104
|
-
@dataclass
|
105
|
-
class ServiceReportHandler(BaseReportHandler):
|
106
|
-
client: ServiceClient
|
107
|
-
host_data: HostData
|
108
|
-
api_name: str | None
|
109
|
-
location: str
|
110
|
-
base_url: str | None
|
111
|
-
started_at: str
|
112
|
-
telemetry: bool
|
113
|
-
out_queue: Queue
|
114
|
-
in_queue: Queue = field(default_factory=Queue)
|
115
|
-
worker: threading.Thread = field(init=False)
|
116
|
-
|
117
|
-
def __post_init__(self) -> None:
|
118
|
-
self.worker = threading.Thread(
|
119
|
-
target=write_remote,
|
120
|
-
kwargs={
|
121
|
-
"client": self.client,
|
122
|
-
"host_data": self.host_data,
|
123
|
-
"api_name": self.api_name,
|
124
|
-
"location": self.location,
|
125
|
-
"base_url": self.base_url,
|
126
|
-
"started_at": self.started_at,
|
127
|
-
"in_queue": self.in_queue,
|
128
|
-
"out_queue": self.out_queue,
|
129
|
-
"usage_data": usage.collect() if self.telemetry else None,
|
130
|
-
},
|
131
|
-
)
|
132
|
-
self.worker.start()
|
133
|
-
|
134
|
-
|
135
|
-
@enum.unique
|
136
|
-
class ConsumeResult(enum.Enum):
|
137
|
-
NORMAL = 1
|
138
|
-
INTERRUPT = 2
|
139
|
-
|
140
|
-
|
141
|
-
def consume_events(writer: ReportWriter, in_queue: Queue) -> ConsumeResult:
|
142
|
-
while True:
|
143
|
-
event = in_queue.get()
|
144
|
-
if event is STOP_MARKER or isinstance(event, (Interrupted, InternalError)):
|
145
|
-
# If the run is interrupted, or there is an internal error - do not send the report
|
146
|
-
return ConsumeResult.INTERRUPT
|
147
|
-
# Add every event to the report
|
148
|
-
if isinstance(event, Initialized):
|
149
|
-
writer.add_json_file("schema.json", event.schema)
|
150
|
-
writer.add_event(event)
|
151
|
-
if event.is_terminal:
|
152
|
-
break
|
153
|
-
return ConsumeResult.NORMAL
|
154
|
-
|
155
|
-
|
156
|
-
def write_remote(
|
157
|
-
client: ServiceClient,
|
158
|
-
host_data: HostData,
|
159
|
-
api_name: str | None,
|
160
|
-
location: str,
|
161
|
-
base_url: str | None,
|
162
|
-
started_at: str,
|
163
|
-
in_queue: Queue,
|
164
|
-
out_queue: Queue,
|
165
|
-
usage_data: dict[str, Any] | None,
|
166
|
-
) -> None:
|
167
|
-
"""Create a compressed ``tar.gz`` file during the run & upload it to Schemathesis.io when the run is finished."""
|
168
|
-
payload = BytesIO()
|
169
|
-
try:
|
170
|
-
with tarfile.open(mode="w:gz", fileobj=payload) as tar:
|
171
|
-
writer = ReportWriter(tar)
|
172
|
-
ci_environment = ci.environment()
|
173
|
-
writer.add_metadata(
|
174
|
-
api_name=api_name,
|
175
|
-
location=location,
|
176
|
-
base_url=base_url,
|
177
|
-
started_at=started_at,
|
178
|
-
metadata=Metadata(),
|
179
|
-
ci_environment=ci_environment,
|
180
|
-
usage_data=usage_data,
|
181
|
-
)
|
182
|
-
if consume_events(writer, in_queue) == ConsumeResult.INTERRUPT:
|
183
|
-
return
|
184
|
-
data = payload.getvalue()
|
185
|
-
out_queue.put(events.Metadata(size=len(data), ci_environment=ci_environment))
|
186
|
-
provider = ci_environment.provider if ci_environment is not None else None
|
187
|
-
response = client.upload_report(data, host_data.correlation_id, provider)
|
188
|
-
event: events.Event
|
189
|
-
if isinstance(response, UploadResponse):
|
190
|
-
host_data.store_correlation_id(response.correlation_id)
|
191
|
-
event = events.Completed(message=response.message, next_url=response.next_url)
|
192
|
-
else:
|
193
|
-
event = events.Failed(detail=response.detail)
|
194
|
-
out_queue.put(event)
|
195
|
-
except Exception as exc:
|
196
|
-
out_queue.put(events.Error(exc))
|
197
|
-
|
198
|
-
|
199
|
-
@dataclass
|
200
|
-
class FileReportHandler(BaseReportHandler):
|
201
|
-
file_handle: click.utils.LazyFile
|
202
|
-
api_name: str | None
|
203
|
-
location: str
|
204
|
-
base_url: str | None
|
205
|
-
started_at: str
|
206
|
-
telemetry: bool
|
207
|
-
out_queue: Queue
|
208
|
-
in_queue: Queue = field(default_factory=Queue)
|
209
|
-
worker: threading.Thread = field(init=False)
|
210
|
-
|
211
|
-
def __post_init__(self) -> None:
|
212
|
-
self.worker = threading.Thread(
|
213
|
-
target=write_file,
|
214
|
-
kwargs={
|
215
|
-
"file_handle": self.file_handle,
|
216
|
-
"api_name": self.api_name,
|
217
|
-
"location": self.location,
|
218
|
-
"base_url": self.base_url,
|
219
|
-
"started_at": self.started_at,
|
220
|
-
"in_queue": self.in_queue,
|
221
|
-
"out_queue": self.out_queue,
|
222
|
-
"usage_data": usage.collect() if self.telemetry else None,
|
223
|
-
},
|
224
|
-
)
|
225
|
-
self.worker.start()
|
226
|
-
|
227
|
-
|
228
|
-
def write_file(
|
229
|
-
file_handle: click.utils.LazyFile,
|
230
|
-
api_name: str | None,
|
231
|
-
location: str,
|
232
|
-
base_url: str | None,
|
233
|
-
started_at: str,
|
234
|
-
in_queue: Queue,
|
235
|
-
out_queue: Queue,
|
236
|
-
usage_data: dict[str, Any] | None,
|
237
|
-
) -> None:
|
238
|
-
with file_handle.open() as fileobj, tarfile.open(mode="w:gz", fileobj=fileobj) as tar:
|
239
|
-
writer = ReportWriter(tar)
|
240
|
-
ci_environment = ci.environment()
|
241
|
-
writer.add_metadata(
|
242
|
-
api_name=api_name,
|
243
|
-
location=location,
|
244
|
-
base_url=base_url,
|
245
|
-
started_at=started_at,
|
246
|
-
metadata=Metadata(),
|
247
|
-
ci_environment=ci_environment,
|
248
|
-
usage_data=usage_data,
|
249
|
-
)
|
250
|
-
result = consume_events(writer, in_queue)
|
251
|
-
if result == ConsumeResult.INTERRUPT:
|
252
|
-
with suppress(OSError):
|
253
|
-
os.remove(file_handle.name)
|
254
|
-
else:
|
255
|
-
out_queue.put(events.Metadata(size=os.path.getsize(file_handle.name), ci_environment=ci_environment))
|