schemathesis 3.21.2__py3-none-any.whl → 3.22.1__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 +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/service/client.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import hashlib
|
|
2
3
|
import http
|
|
3
4
|
from dataclasses import asdict
|
|
4
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
5
6
|
from urllib.parse import urljoin
|
|
6
7
|
|
|
7
8
|
import requests
|
|
@@ -11,7 +12,15 @@ from ..constants import USER_AGENT
|
|
|
11
12
|
from .ci import CIProvider
|
|
12
13
|
from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
|
|
13
14
|
from .metadata import Metadata
|
|
14
|
-
from .models import
|
|
15
|
+
from .models import (
|
|
16
|
+
ProjectDetails,
|
|
17
|
+
AuthResponse,
|
|
18
|
+
FailedUploadResponse,
|
|
19
|
+
UploadResponse,
|
|
20
|
+
UploadSource,
|
|
21
|
+
ProjectEnvironment,
|
|
22
|
+
Specification,
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
def response_hook(response: requests.Response, **_kwargs: Any) -> None:
|
|
@@ -22,7 +31,7 @@ def response_hook(response: requests.Response, **_kwargs: Any) -> None:
|
|
|
22
31
|
class ServiceClient(requests.Session):
|
|
23
32
|
"""A more convenient session to send requests to Schemathesis.io."""
|
|
24
33
|
|
|
25
|
-
def __init__(self, base_url: str, token:
|
|
34
|
+
def __init__(self, base_url: str, token: str | None, *, timeout: int = REQUEST_TIMEOUT, verify: bool = True):
|
|
26
35
|
super().__init__()
|
|
27
36
|
self.timeout = timeout
|
|
28
37
|
self.verify = verify
|
|
@@ -43,11 +52,22 @@ class ServiceClient(requests.Session):
|
|
|
43
52
|
url = urljoin(self.base_url, url)
|
|
44
53
|
return super().request(method, url, *args, **kwargs)
|
|
45
54
|
|
|
46
|
-
def get_api_details(self, name: str) ->
|
|
55
|
+
def get_api_details(self, name: str) -> ProjectDetails:
|
|
47
56
|
"""Get information about an API."""
|
|
48
|
-
response = self.get(f"/
|
|
57
|
+
response = self.get(f"/cli/projects/{name}/")
|
|
49
58
|
data = response.json()
|
|
50
|
-
return
|
|
59
|
+
return ProjectDetails(
|
|
60
|
+
environments=[
|
|
61
|
+
ProjectEnvironment(
|
|
62
|
+
url=environment["url"],
|
|
63
|
+
name=environment["name"],
|
|
64
|
+
description=environment["description"],
|
|
65
|
+
is_default=environment["is_default"],
|
|
66
|
+
)
|
|
67
|
+
for environment in data["environments"]
|
|
68
|
+
],
|
|
69
|
+
specification=Specification(schema=data["specification"]["schema"]),
|
|
70
|
+
)
|
|
51
71
|
|
|
52
72
|
def login(self, metadata: Metadata) -> AuthResponse:
|
|
53
73
|
"""Send a login request."""
|
|
@@ -58,10 +78,10 @@ class ServiceClient(requests.Session):
|
|
|
58
78
|
def upload_report(
|
|
59
79
|
self,
|
|
60
80
|
report: bytes,
|
|
61
|
-
correlation_id:
|
|
62
|
-
ci_provider:
|
|
81
|
+
correlation_id: str | None = None,
|
|
82
|
+
ci_provider: CIProvider | None = None,
|
|
63
83
|
source: UploadSource = UploadSource.DEFAULT,
|
|
64
|
-
) ->
|
|
84
|
+
) -> UploadResponse | FailedUploadResponse:
|
|
65
85
|
"""Upload test run report to Schemathesis.io."""
|
|
66
86
|
headers = {
|
|
67
87
|
"Content-Type": "application/x-gtar",
|
schemathesis/service/events.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from . import ci
|
|
5
5
|
from ..exceptions import format_exception
|
|
@@ -21,7 +21,7 @@ class Metadata(Event):
|
|
|
21
21
|
"""Meta-information about the report."""
|
|
22
22
|
|
|
23
23
|
size: int
|
|
24
|
-
ci_environment:
|
|
24
|
+
ci_environment: ci.Environment | None
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@dataclass
|
schemathesis/service/hosts.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Work with stored auth data."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import enum
|
|
3
4
|
import tempfile
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
import tomli
|
|
9
10
|
import tomli_w
|
|
@@ -19,11 +20,11 @@ class HostData:
|
|
|
19
20
|
hostname: str
|
|
20
21
|
hosts_file: PathLike
|
|
21
22
|
|
|
22
|
-
def load(self) ->
|
|
23
|
+
def load(self) -> dict[str, Any]:
|
|
23
24
|
return load(self.hosts_file).get(self.hostname, {})
|
|
24
25
|
|
|
25
26
|
@property
|
|
26
|
-
def correlation_id(self) ->
|
|
27
|
+
def correlation_id(self) -> str | None:
|
|
27
28
|
return self.load().get("correlation_id")
|
|
28
29
|
|
|
29
30
|
def store_correlation_id(self, correlation_id: str) -> None:
|
|
@@ -43,7 +44,7 @@ def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = D
|
|
|
43
44
|
_dump_hosts(hosts_file, hosts)
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def load(path: PathLike) ->
|
|
47
|
+
def load(path: PathLike) -> dict[str, Any]:
|
|
47
48
|
"""Load the given hosts file.
|
|
48
49
|
|
|
49
50
|
Return an empty dict if it doesn't exist.
|
|
@@ -67,7 +68,7 @@ def _try_make_config_directory(path: PathLike) -> None:
|
|
|
67
68
|
pass
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) ->
|
|
71
|
+
def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
|
|
71
72
|
"""Load all data associated with a hostname."""
|
|
72
73
|
return load(hosts_file).get(hostname, {})
|
|
73
74
|
|
|
@@ -97,7 +98,7 @@ def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOST
|
|
|
97
98
|
return RemoveAuth.error
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) ->
|
|
101
|
+
def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
|
|
101
102
|
"""Load a token for a host."""
|
|
102
103
|
return load_for_host(hostname, hosts_file).get("token")
|
|
103
104
|
|
|
@@ -107,7 +108,7 @@ def get_temporary_hosts_file() -> str:
|
|
|
107
108
|
return str(temporary_dir / "schemathesis-hosts.toml")
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
def _dump_hosts(path: PathLike, hosts:
|
|
111
|
+
def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
|
|
111
112
|
"""Write hosts data to a file."""
|
|
112
113
|
with open(path, "wb") as fd:
|
|
113
114
|
tomli_w.dump(hosts, fd)
|
schemathesis/service/metadata.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Useful info to collect from CLI usage."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
2
4
|
import platform
|
|
3
5
|
from dataclasses import dataclass, field
|
|
4
6
|
|
|
5
7
|
from ..constants import SCHEMATHESIS_VERSION
|
|
8
|
+
from .constants import DOCKER_IMAGE_ENV_VAR
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
@dataclass
|
|
@@ -39,3 +42,5 @@ class Metadata:
|
|
|
39
42
|
interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
|
|
40
43
|
# CLI info itself.
|
|
41
44
|
cli: CliMetadata = field(default_factory=CliMetadata)
|
|
45
|
+
# Used Docker image if any
|
|
46
|
+
docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
|
schemathesis/service/models.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class UploadSource(str, Enum):
|
|
@@ -9,9 +10,26 @@ class UploadSource(str, Enum):
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass
|
|
12
|
-
class
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
class ProjectDetails:
|
|
14
|
+
environments: list[ProjectEnvironment]
|
|
15
|
+
specification: Specification
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def default_environment(self) -> ProjectEnvironment | None:
|
|
19
|
+
return next((env for env in self.environments if env.is_default), None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ProjectEnvironment:
|
|
24
|
+
url: str
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
is_default: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Specification:
|
|
32
|
+
schema: dict[str, Any]
|
|
15
33
|
|
|
16
34
|
|
|
17
35
|
@dataclass
|
schemathesis/service/report.py
CHANGED
|
@@ -9,7 +9,7 @@ from contextlib import suppress
|
|
|
9
9
|
from dataclasses import asdict, dataclass, field
|
|
10
10
|
from io import BytesIO
|
|
11
11
|
from queue import Queue
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
@@ -51,13 +51,13 @@ class ReportWriter:
|
|
|
51
51
|
def add_metadata(
|
|
52
52
|
self,
|
|
53
53
|
*,
|
|
54
|
-
api_name:
|
|
54
|
+
api_name: str | None,
|
|
55
55
|
location: str,
|
|
56
|
-
base_url: str,
|
|
56
|
+
base_url: str | None,
|
|
57
57
|
started_at: str,
|
|
58
58
|
metadata: Metadata,
|
|
59
|
-
ci_environment:
|
|
60
|
-
usage_data:
|
|
59
|
+
ci_environment: ci.Environment | None,
|
|
60
|
+
usage_data: dict[str, Any] | None,
|
|
61
61
|
) -> None:
|
|
62
62
|
data = {
|
|
63
63
|
# API identifier on the Schemathesis.io side (optional)
|
|
@@ -105,9 +105,9 @@ class BaseReportHandler(EventHandler):
|
|
|
105
105
|
class ServiceReportHandler(BaseReportHandler):
|
|
106
106
|
client: ServiceClient
|
|
107
107
|
host_data: HostData
|
|
108
|
-
api_name:
|
|
108
|
+
api_name: str | None
|
|
109
109
|
location: str
|
|
110
|
-
base_url:
|
|
110
|
+
base_url: str | None
|
|
111
111
|
started_at: str
|
|
112
112
|
telemetry: bool
|
|
113
113
|
out_queue: Queue
|
|
@@ -156,13 +156,13 @@ def consume_events(writer: ReportWriter, in_queue: Queue) -> ConsumeResult:
|
|
|
156
156
|
def write_remote(
|
|
157
157
|
client: ServiceClient,
|
|
158
158
|
host_data: HostData,
|
|
159
|
-
api_name:
|
|
159
|
+
api_name: str | None,
|
|
160
160
|
location: str,
|
|
161
|
-
base_url: str,
|
|
161
|
+
base_url: str | None,
|
|
162
162
|
started_at: str,
|
|
163
163
|
in_queue: Queue,
|
|
164
164
|
out_queue: Queue,
|
|
165
|
-
usage_data:
|
|
165
|
+
usage_data: dict[str, Any] | None,
|
|
166
166
|
) -> None:
|
|
167
167
|
"""Create a compressed ``tar.gz`` file during the run & upload it to Schemathesis.io when the run is finished."""
|
|
168
168
|
payload = BytesIO()
|
|
@@ -199,9 +199,9 @@ def write_remote(
|
|
|
199
199
|
@dataclass
|
|
200
200
|
class FileReportHandler(BaseReportHandler):
|
|
201
201
|
file_handle: click.utils.LazyFile
|
|
202
|
-
api_name:
|
|
202
|
+
api_name: str | None
|
|
203
203
|
location: str
|
|
204
|
-
base_url:
|
|
204
|
+
base_url: str | None
|
|
205
205
|
started_at: str
|
|
206
206
|
telemetry: bool
|
|
207
207
|
out_queue: Queue
|
|
@@ -227,13 +227,13 @@ class FileReportHandler(BaseReportHandler):
|
|
|
227
227
|
|
|
228
228
|
def write_file(
|
|
229
229
|
file_handle: click.utils.LazyFile,
|
|
230
|
-
api_name:
|
|
230
|
+
api_name: str | None,
|
|
231
231
|
location: str,
|
|
232
|
-
base_url: str,
|
|
232
|
+
base_url: str | None,
|
|
233
233
|
started_at: str,
|
|
234
234
|
in_queue: Queue,
|
|
235
235
|
out_queue: Queue,
|
|
236
|
-
usage_data:
|
|
236
|
+
usage_data: dict[str, Any] | None,
|
|
237
237
|
) -> None:
|
|
238
238
|
with file_handle.open() as fileobj, tarfile.open(mode="w:gz", fileobj=fileobj) as tar:
|
|
239
239
|
writer = ReportWriter(tar)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from dataclasses import asdict
|
|
2
|
-
from typing import Any, Callable, Dict,
|
|
3
|
+
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
3
4
|
|
|
4
5
|
from ..models import Response
|
|
5
6
|
from ..runner import events
|
|
@@ -10,7 +11,7 @@ S = TypeVar("S", bound=events.ExecutionEvent)
|
|
|
10
11
|
SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def serialize_initialized(event: events.Initialized) ->
|
|
14
|
+
def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
|
|
14
15
|
return {
|
|
15
16
|
"operations_count": event.operations_count,
|
|
16
17
|
"location": event.location or "",
|
|
@@ -18,7 +19,7 @@ def serialize_initialized(event: events.Initialized) -> Optional[Dict[str, Any]]
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def serialize_before_execution(event: events.BeforeExecution) ->
|
|
22
|
+
def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
|
|
22
23
|
return {
|
|
23
24
|
"correlation_id": event.correlation_id,
|
|
24
25
|
"verbose_name": event.verbose_name,
|
|
@@ -26,7 +27,7 @@ def serialize_before_execution(event: events.BeforeExecution) -> Optional[Dict[s
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def _serialize_case(case: SerializedCase) ->
|
|
30
|
+
def _serialize_case(case: SerializedCase) -> dict[str, Any]:
|
|
30
31
|
return {
|
|
31
32
|
"verbose_name": case.verbose_name,
|
|
32
33
|
"path_template": case.path_template,
|
|
@@ -37,7 +38,7 @@ def _serialize_case(case: SerializedCase) -> Dict[str, Any]:
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def _serialize_response(response: Response) ->
|
|
41
|
+
def _serialize_response(response: Response) -> dict[str, Any]:
|
|
41
42
|
return {
|
|
42
43
|
"status_code": response.status_code,
|
|
43
44
|
"headers": response.headers,
|
|
@@ -47,7 +48,7 @@ def _serialize_response(response: Response) -> Dict[str, Any]:
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def serialize_after_execution(event: events.AfterExecution) ->
|
|
51
|
+
def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
|
|
51
52
|
return {
|
|
52
53
|
"correlation_id": event.correlation_id,
|
|
53
54
|
"verbose_name": event.verbose_name,
|
|
@@ -76,22 +77,17 @@ def serialize_after_execution(event: events.AfterExecution) -> Optional[Dict[str
|
|
|
76
77
|
}
|
|
77
78
|
for check in event.result.checks
|
|
78
79
|
],
|
|
79
|
-
"errors": [
|
|
80
|
-
|
|
81
|
-
"exception": error.exception,
|
|
82
|
-
"exception_with_traceback": error.exception_with_traceback,
|
|
83
|
-
}
|
|
84
|
-
for error in event.result.errors
|
|
85
|
-
],
|
|
80
|
+
"errors": [asdict(error) for error in event.result.errors],
|
|
81
|
+
"skip_reason": event.result.skip_reason,
|
|
86
82
|
},
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
|
|
90
|
-
def serialize_interrupted(_: events.Interrupted) ->
|
|
86
|
+
def serialize_interrupted(_: events.Interrupted) -> dict[str, Any] | None:
|
|
91
87
|
return None
|
|
92
88
|
|
|
93
89
|
|
|
94
|
-
def serialize_internal_error(event: events.InternalError) ->
|
|
90
|
+
def serialize_internal_error(event: events.InternalError) -> dict[str, Any] | None:
|
|
95
91
|
return {
|
|
96
92
|
"type": event.type.value,
|
|
97
93
|
"subtype": event.subtype.value if event.subtype else event.subtype,
|
|
@@ -104,7 +100,7 @@ def serialize_internal_error(event: events.InternalError) -> Optional[Dict[str,
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
|
|
107
|
-
def serialize_finished(event: events.Finished) ->
|
|
103
|
+
def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
|
|
108
104
|
return {
|
|
109
105
|
"generic_errors": [
|
|
110
106
|
{
|
|
@@ -131,14 +127,14 @@ SERIALIZER_MAP = {
|
|
|
131
127
|
def serialize_event(
|
|
132
128
|
event: events.ExecutionEvent,
|
|
133
129
|
*,
|
|
134
|
-
on_initialized:
|
|
135
|
-
on_before_execution:
|
|
136
|
-
on_after_execution:
|
|
137
|
-
on_interrupted:
|
|
138
|
-
on_internal_error:
|
|
139
|
-
on_finished:
|
|
140
|
-
extra:
|
|
141
|
-
) ->
|
|
130
|
+
on_initialized: SerializeFunc | None = None,
|
|
131
|
+
on_before_execution: SerializeFunc | None = None,
|
|
132
|
+
on_after_execution: SerializeFunc | None = None,
|
|
133
|
+
on_interrupted: SerializeFunc | None = None,
|
|
134
|
+
on_internal_error: SerializeFunc | None = None,
|
|
135
|
+
on_finished: SerializeFunc | None = None,
|
|
136
|
+
extra: dict[str, Any] | None = None,
|
|
137
|
+
) -> dict[str, dict[str, Any] | None]:
|
|
142
138
|
"""Turn an event into JSON-serializable structure."""
|
|
143
139
|
# Use the explicitly provided serializer for this event and fallback to default one if it is not provided
|
|
144
140
|
serializer = {
|
|
@@ -163,7 +159,7 @@ def serialize_event(
|
|
|
163
159
|
return {event.__class__.__name__: data}
|
|
164
160
|
|
|
165
161
|
|
|
166
|
-
def stringify_path_parameters(path_parameters:
|
|
162
|
+
def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
|
|
167
163
|
"""Cast all path parameter values to strings.
|
|
168
164
|
|
|
169
165
|
Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
|
|
@@ -171,14 +167,14 @@ def stringify_path_parameters(path_parameters: Optional[Dict[str, Any]]) -> Dict
|
|
|
171
167
|
return {key: str(value) for key, value in (path_parameters or {}).items()}
|
|
172
168
|
|
|
173
169
|
|
|
174
|
-
def prepare_query(query:
|
|
170
|
+
def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
|
|
175
171
|
"""Convert all query values to list of strings.
|
|
176
172
|
|
|
177
173
|
Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
|
|
178
174
|
It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
|
|
179
175
|
"""
|
|
180
176
|
|
|
181
|
-
def to_list_of_strings(value: Any) ->
|
|
177
|
+
def to_list_of_strings(value: Any) -> list[str]:
|
|
182
178
|
if isinstance(value, list):
|
|
183
179
|
return list(map(str, value))
|
|
184
180
|
if isinstance(value, str):
|
schemathesis/service/usage.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import sys
|
|
2
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
from click.types import StringParamType
|
|
@@ -7,14 +8,14 @@ from click.types import StringParamType
|
|
|
7
8
|
from .. import cli, hooks
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def collect(args:
|
|
11
|
+
def collect(args: list[str] | None = None) -> dict[str, Any] | None:
|
|
11
12
|
"""Collect anonymized CLI usage data."""
|
|
12
|
-
context:
|
|
13
|
+
context: click.Context | None = click.get_current_context(silent=True)
|
|
13
14
|
if context is not None and not sys.argv[0].endswith("pytest"):
|
|
14
15
|
args = args or sys.argv[2:]
|
|
15
16
|
parameters, _, types = parse_cli_args(context, args)
|
|
16
|
-
parameters_data:
|
|
17
|
-
used_headers:
|
|
17
|
+
parameters_data: dict[str, dict[str, Any]] = {}
|
|
18
|
+
used_headers: list[str] = []
|
|
18
19
|
schema = parameters["schema"]
|
|
19
20
|
app = parameters.get("app")
|
|
20
21
|
if not schema:
|
|
@@ -43,7 +44,7 @@ def collect(args: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
|
|
43
44
|
return None
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def _collect_option(option: str, option_type: click.Parameter, used_headers:
|
|
47
|
+
def _collect_option(option: str, option_type: click.Parameter, used_headers: list[str], value: Any) -> dict[str, Any]:
|
|
47
48
|
entry = {}
|
|
48
49
|
if isinstance(option_type.type, (StringParamType, click.types.File)):
|
|
49
50
|
if option == "headers" and value:
|
|
@@ -59,6 +60,6 @@ def _collect_option(option: str, option_type: click.Parameter, used_headers: Lis
|
|
|
59
60
|
return entry
|
|
60
61
|
|
|
61
62
|
|
|
62
|
-
def parse_cli_args(context: click.Context, args:
|
|
63
|
+
def parse_cli_args(context: click.Context, args: list[str]) -> tuple[dict[str, Any], list, list[click.Parameter]]:
|
|
63
64
|
parser = cli.run.make_parser(context)
|
|
64
65
|
return parser.parse_args(args=args)
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
import pathlib
|
|
3
3
|
from functools import lru_cache
|
|
4
4
|
from json import JSONDecodeError
|
|
5
|
-
from typing import IO, Any, Callable, Dict,
|
|
5
|
+
from typing import IO, Any, Callable, Dict, cast, TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from ...code_samples import CodeSampleStyle
|
|
8
8
|
from ...generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, DataGenerationMethodInput
|
|
@@ -22,14 +22,14 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from .schemas import GraphQLSchema
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
@lru_cache
|
|
25
|
+
@lru_cache
|
|
26
26
|
def get_introspection_query() -> str:
|
|
27
27
|
import graphql
|
|
28
28
|
|
|
29
29
|
return graphql.get_introspection_query()
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
@lru_cache
|
|
32
|
+
@lru_cache
|
|
33
33
|
def get_introspection_query_ast() -> DocumentNode:
|
|
34
34
|
import graphql
|
|
35
35
|
|
|
@@ -41,10 +41,10 @@ def from_path(
|
|
|
41
41
|
path: PathLike,
|
|
42
42
|
*,
|
|
43
43
|
app: Any = None,
|
|
44
|
-
base_url:
|
|
44
|
+
base_url: str | None = None,
|
|
45
45
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
46
46
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
47
|
-
rate_limit:
|
|
47
|
+
rate_limit: str | None = None,
|
|
48
48
|
encoding: str = "utf8",
|
|
49
49
|
sanitize_output: bool = True,
|
|
50
50
|
) -> GraphQLSchema:
|
|
@@ -66,7 +66,7 @@ def from_path(
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def extract_schema_from_response(response: GenericResponse) ->
|
|
69
|
+
def extract_schema_from_response(response: GenericResponse) -> dict[str, Any]:
|
|
70
70
|
from requests import Response
|
|
71
71
|
|
|
72
72
|
try:
|
|
@@ -86,12 +86,12 @@ def from_url(
|
|
|
86
86
|
url: str,
|
|
87
87
|
*,
|
|
88
88
|
app: Any = None,
|
|
89
|
-
base_url:
|
|
90
|
-
port:
|
|
89
|
+
base_url: str | None = None,
|
|
90
|
+
port: int | None = None,
|
|
91
91
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
92
92
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
93
|
-
wait_for_schema:
|
|
94
|
-
rate_limit:
|
|
93
|
+
wait_for_schema: float | None = None,
|
|
94
|
+
rate_limit: str | None = None,
|
|
95
95
|
sanitize_output: bool = True,
|
|
96
96
|
**kwargs: Any,
|
|
97
97
|
) -> GraphQLSchema:
|
|
@@ -145,14 +145,14 @@ def from_url(
|
|
|
145
145
|
|
|
146
146
|
|
|
147
147
|
def from_file(
|
|
148
|
-
file:
|
|
148
|
+
file: IO[str] | str,
|
|
149
149
|
*,
|
|
150
150
|
app: Any = None,
|
|
151
|
-
base_url:
|
|
151
|
+
base_url: str | None = None,
|
|
152
152
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
153
153
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
154
|
-
location:
|
|
155
|
-
rate_limit:
|
|
154
|
+
location: str | None = None,
|
|
155
|
+
rate_limit: str | None = None,
|
|
156
156
|
sanitize_output: bool = True,
|
|
157
157
|
) -> GraphQLSchema:
|
|
158
158
|
"""Load GraphQL schema from a file descriptor or a string.
|
|
@@ -165,7 +165,14 @@ def from_file(
|
|
|
165
165
|
data = file
|
|
166
166
|
else:
|
|
167
167
|
data = file.read()
|
|
168
|
-
|
|
168
|
+
try:
|
|
169
|
+
document = graphql.build_schema(data)
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
raise SchemaError(
|
|
172
|
+
SchemaErrorType.GRAPHQL_INVALID_SCHEMA,
|
|
173
|
+
"The provided API schema does not appear to be a valid GraphQL schema",
|
|
174
|
+
extras=[entry for entry in str(exc).splitlines() if entry],
|
|
175
|
+
) from exc
|
|
169
176
|
result = graphql.execute(document, get_introspection_query_ast())
|
|
170
177
|
# TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
|
|
171
178
|
result = cast(graphql.ExecutionResult, result)
|
|
@@ -187,14 +194,14 @@ def from_file(
|
|
|
187
194
|
|
|
188
195
|
|
|
189
196
|
def from_dict(
|
|
190
|
-
raw_schema:
|
|
197
|
+
raw_schema: dict[str, Any],
|
|
191
198
|
*,
|
|
192
199
|
app: Any = None,
|
|
193
|
-
base_url:
|
|
194
|
-
location:
|
|
200
|
+
base_url: str | None = None,
|
|
201
|
+
location: str | None = None,
|
|
195
202
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
196
203
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
197
|
-
rate_limit:
|
|
204
|
+
rate_limit: str | None = None,
|
|
198
205
|
sanitize_output: bool = True,
|
|
199
206
|
) -> GraphQLSchema:
|
|
200
207
|
"""Load GraphQL schema from a Python dictionary.
|
|
@@ -212,7 +219,7 @@ def from_dict(
|
|
|
212
219
|
if "data" in raw_schema:
|
|
213
220
|
raw_schema = raw_schema["data"]
|
|
214
221
|
dispatch("before_load_schema", hook_context, raw_schema)
|
|
215
|
-
rate_limiter:
|
|
222
|
+
rate_limiter: Limiter | None = None
|
|
216
223
|
if rate_limit is not None:
|
|
217
224
|
rate_limiter = build_limiter(rate_limit)
|
|
218
225
|
instance = GraphQLSchema(
|
|
@@ -233,10 +240,10 @@ def from_wsgi(
|
|
|
233
240
|
schema_path: str,
|
|
234
241
|
app: Any,
|
|
235
242
|
*,
|
|
236
|
-
base_url:
|
|
243
|
+
base_url: str | None = None,
|
|
237
244
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
238
245
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
239
|
-
rate_limit:
|
|
246
|
+
rate_limit: str | None = None,
|
|
240
247
|
sanitize_output: bool = True,
|
|
241
248
|
**kwargs: Any,
|
|
242
249
|
) -> GraphQLSchema:
|
|
@@ -272,10 +279,10 @@ def from_asgi(
|
|
|
272
279
|
schema_path: str,
|
|
273
280
|
app: Any,
|
|
274
281
|
*,
|
|
275
|
-
base_url:
|
|
282
|
+
base_url: str | None = None,
|
|
276
283
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
277
284
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
278
|
-
rate_limit:
|
|
285
|
+
rate_limit: str | None = None,
|
|
279
286
|
sanitize_output: bool = True,
|
|
280
287
|
**kwargs: Any,
|
|
281
288
|
) -> GraphQLSchema:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
2
3
|
|
|
3
4
|
if TYPE_CHECKING:
|
|
4
5
|
from graphql import ValueNode
|
|
@@ -17,7 +18,7 @@ __all__ = [ # noqa: F822
|
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def __getattr__(name: str) ->
|
|
21
|
+
def __getattr__(name: str) -> ValueNode | None:
|
|
21
22
|
if name in __all__:
|
|
22
23
|
import hypothesis_graphql.nodes
|
|
23
24
|
|