schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- 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 +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -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} +103 -174
- schemathesis/cli/constants.py +5 -52
- 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} +39 -10
- 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 -5
- 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 +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- 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 +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- 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 +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -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} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- 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} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1 +0,0 @@
|
|
1
|
-
from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from ...schemas import APIOperation, APIOperationMap
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class OperationCache:
|
12
|
+
_maps: dict[str, APIOperationMap] = field(default_factory=dict)
|
13
|
+
_operations: dict[str, APIOperation] = field(default_factory=dict)
|
14
|
+
|
15
|
+
def get_map(self, key: str) -> APIOperationMap | None:
|
16
|
+
return self._maps.get(key)
|
17
|
+
|
18
|
+
def insert_map(self, key: str, value: APIOperationMap) -> None:
|
19
|
+
self._maps[key] = value
|
20
|
+
|
21
|
+
def get_operation(self, key: str) -> APIOperation | None:
|
22
|
+
return self._operations.get(key)
|
23
|
+
|
24
|
+
def insert_operation(self, key: str, value: APIOperation) -> None:
|
25
|
+
self._operations[key] = value
|
@@ -3,8 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from functools import lru_cache
|
4
4
|
from typing import TYPE_CHECKING
|
5
5
|
|
6
|
-
|
7
|
-
from ...exceptions import UsageError
|
6
|
+
from schemathesis.core.errors import IncorrectUsage
|
8
7
|
|
9
8
|
if TYPE_CHECKING:
|
10
9
|
import graphql
|
@@ -22,18 +21,21 @@ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
22
21
|
from hypothesis.strategies import SearchStrategy
|
23
22
|
|
24
23
|
if not isinstance(name, str):
|
25
|
-
raise
|
24
|
+
raise IncorrectUsage(f"Scalar name {name!r} must be a string")
|
26
25
|
if not isinstance(strategy, SearchStrategy):
|
27
|
-
raise
|
26
|
+
raise IncorrectUsage(
|
27
|
+
f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
|
28
|
+
)
|
28
29
|
CUSTOM_SCALARS[name] = strategy
|
29
30
|
|
30
31
|
|
31
32
|
@lru_cache
|
32
33
|
def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
|
33
34
|
"""Get all extra GraphQL strategies."""
|
34
|
-
from . import nodes
|
35
35
|
from hypothesis import strategies as st
|
36
36
|
|
37
|
+
from . import nodes
|
38
|
+
|
37
39
|
dates = st.dates().map(str)
|
38
40
|
times = st.times().map("%sZ".__mod__)
|
39
41
|
|
@@ -1,54 +1,56 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import enum
|
4
|
+
import time
|
4
5
|
from dataclasses import dataclass, field
|
5
6
|
from difflib import get_close_matches
|
6
7
|
from enum import unique
|
8
|
+
from types import SimpleNamespace
|
7
9
|
from typing import (
|
10
|
+
TYPE_CHECKING,
|
8
11
|
Any,
|
9
12
|
Callable,
|
10
13
|
Generator,
|
11
|
-
Sequence,
|
12
|
-
TypeVar,
|
13
|
-
cast,
|
14
|
-
TYPE_CHECKING,
|
15
|
-
NoReturn,
|
16
|
-
MutableMapping,
|
17
14
|
Iterator,
|
15
|
+
Mapping,
|
16
|
+
NoReturn,
|
17
|
+
Union,
|
18
|
+
cast,
|
18
19
|
)
|
19
|
-
from urllib.parse import urlsplit
|
20
|
+
from urllib.parse import urlsplit
|
20
21
|
|
21
22
|
import graphql
|
22
|
-
import requests
|
23
|
-
from graphql import GraphQLNamedType
|
24
23
|
from hypothesis import strategies as st
|
25
|
-
from hypothesis.strategies import SearchStrategy
|
26
24
|
from hypothesis_graphql import strategies as gql_st
|
27
25
|
from requests.structures import CaseInsensitiveDict
|
28
26
|
|
29
|
-
from
|
30
|
-
from
|
31
|
-
from
|
32
|
-
from
|
33
|
-
from
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
27
|
+
from schemathesis.core import NOT_SET, NotSet, Specification
|
28
|
+
from schemathesis.core.errors import InvalidSchema, OperationNotFound
|
29
|
+
from schemathesis.core.result import Ok, Result
|
30
|
+
from schemathesis.generation.case import Case
|
31
|
+
from schemathesis.generation.meta import (
|
32
|
+
CaseMetadata,
|
33
|
+
ComponentInfo,
|
34
|
+
ComponentKind,
|
35
|
+
ExplicitPhaseData,
|
36
|
+
GeneratePhaseData,
|
37
|
+
GenerationInfo,
|
38
|
+
PhaseInfo,
|
39
|
+
TestPhase,
|
42
40
|
)
|
43
|
-
|
44
|
-
from ...
|
45
|
-
from ...
|
46
|
-
from ...
|
47
|
-
from ...
|
41
|
+
|
42
|
+
from ... import auths
|
43
|
+
from ...generation import GenerationConfig, GenerationMode
|
44
|
+
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
45
|
+
from ...schemas import APIOperation, APIOperationMap, ApiOperationsCount, BaseSchema, OperationDefinition
|
46
|
+
from ..openapi.constants import LOCATION_TO_CONTAINER
|
47
|
+
from ._cache import OperationCache
|
48
48
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
49
49
|
|
50
50
|
if TYPE_CHECKING:
|
51
|
-
from
|
51
|
+
from hypothesis.strategies import SearchStrategy
|
52
|
+
|
53
|
+
from schemathesis.auths import AuthStorage
|
52
54
|
|
53
55
|
|
54
56
|
@unique
|
@@ -58,73 +60,13 @@ class RootType(enum.Enum):
|
|
58
60
|
|
59
61
|
|
60
62
|
@dataclass(repr=False)
|
61
|
-
class GraphQLCase(Case):
|
62
|
-
def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
63
|
-
final_headers = self._get_headers(headers)
|
64
|
-
base_url = self._get_base_url(base_url)
|
65
|
-
# Replace the path, in case if the user provided any path parameters via hooks
|
66
|
-
parts = list(urlsplit(base_url))
|
67
|
-
parts[2] = self.formatted_path
|
68
|
-
kwargs: dict[str, Any] = {
|
69
|
-
"method": self.method,
|
70
|
-
"url": urlunsplit(parts),
|
71
|
-
"headers": final_headers,
|
72
|
-
"cookies": self.cookies,
|
73
|
-
"params": self.query,
|
74
|
-
}
|
75
|
-
# There is no direct way to have bytes here, but it is a useful pattern to support.
|
76
|
-
# It also unifies GraphQLCase with its Open API counterpart where bytes may come from external examples
|
77
|
-
if isinstance(self.body, bytes):
|
78
|
-
kwargs["data"] = self.body
|
79
|
-
# Assume that the payload is JSON, not raw GraphQL queries
|
80
|
-
kwargs["headers"].setdefault("Content-Type", "application/json")
|
81
|
-
else:
|
82
|
-
kwargs["json"] = {"query": self.body}
|
83
|
-
return kwargs
|
84
|
-
|
85
|
-
def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
86
|
-
final_headers = self._get_headers(headers)
|
87
|
-
return {
|
88
|
-
"method": self.method,
|
89
|
-
"path": self.operation.schema.get_full_path(self.formatted_path),
|
90
|
-
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
91
|
-
"headers": dict(final_headers),
|
92
|
-
"query_string": self.query,
|
93
|
-
"json": {"query": self.body},
|
94
|
-
}
|
95
|
-
|
96
|
-
def validate_response(
|
97
|
-
self,
|
98
|
-
response: GenericResponse,
|
99
|
-
checks: tuple[CheckFunction, ...] = (),
|
100
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
101
|
-
excluded_checks: tuple[CheckFunction, ...] = (),
|
102
|
-
code_sample_style: str | None = None,
|
103
|
-
) -> None:
|
104
|
-
checks = checks or (not_a_server_error,)
|
105
|
-
checks += additional_checks
|
106
|
-
checks = tuple(check for check in checks if check not in excluded_checks)
|
107
|
-
return super().validate_response(response, checks, code_sample_style=code_sample_style)
|
108
|
-
|
109
|
-
def call_asgi(
|
110
|
-
self,
|
111
|
-
app: Any = None,
|
112
|
-
base_url: str | None = None,
|
113
|
-
headers: dict[str, str] | None = None,
|
114
|
-
**kwargs: Any,
|
115
|
-
) -> requests.Response:
|
116
|
-
return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)
|
117
|
-
|
118
|
-
|
119
|
-
C = TypeVar("C", bound=Case)
|
120
|
-
|
121
|
-
|
122
|
-
@dataclass
|
123
63
|
class GraphQLOperationDefinition(OperationDefinition):
|
124
64
|
field_name: str
|
125
65
|
type_: graphql.GraphQLType
|
126
66
|
root_type: RootType
|
127
67
|
|
68
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
69
|
+
|
128
70
|
@property
|
129
71
|
def is_query(self) -> bool:
|
130
72
|
return self.root_type == RootType.QUERY
|
@@ -136,9 +78,37 @@ class GraphQLOperationDefinition(OperationDefinition):
|
|
136
78
|
|
137
79
|
@dataclass
|
138
80
|
class GraphQLSchema(BaseSchema):
|
81
|
+
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
82
|
+
|
139
83
|
def __repr__(self) -> str:
|
140
84
|
return f"<{self.__class__.__name__}>"
|
141
85
|
|
86
|
+
def __iter__(self) -> Iterator[str]:
|
87
|
+
schema = self.client_schema
|
88
|
+
for operation_type in (
|
89
|
+
schema.query_type,
|
90
|
+
schema.mutation_type,
|
91
|
+
):
|
92
|
+
if operation_type is not None:
|
93
|
+
yield operation_type.name
|
94
|
+
|
95
|
+
def _get_operation_map(self, key: str) -> APIOperationMap:
|
96
|
+
cache = self._operation_cache
|
97
|
+
map = cache.get_map(key)
|
98
|
+
if map is not None:
|
99
|
+
return map
|
100
|
+
schema = self.client_schema
|
101
|
+
for root_type, operation_type in (
|
102
|
+
(RootType.QUERY, schema.query_type),
|
103
|
+
(RootType.MUTATION, schema.mutation_type),
|
104
|
+
):
|
105
|
+
if operation_type and operation_type.name == key:
|
106
|
+
map = APIOperationMap(self, {})
|
107
|
+
map._data = FieldMap(map, root_type, operation_type)
|
108
|
+
cache.insert_map(key, map)
|
109
|
+
return map
|
110
|
+
raise KeyError(key)
|
111
|
+
|
142
112
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
143
113
|
raw_schema = self.raw_schema["__schema"]
|
144
114
|
type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
|
@@ -148,25 +118,12 @@ class GraphQLSchema(BaseSchema):
|
|
148
118
|
message += f". Did you mean `{matches[0]}`?"
|
149
119
|
raise OperationNotFound(message=message, item=item) from exc
|
150
120
|
|
151
|
-
def _store_operations(
|
152
|
-
self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
|
153
|
-
) -> dict[str, APIOperationMap]:
|
154
|
-
output: dict[str, APIOperationMap] = {}
|
155
|
-
for result in operations:
|
156
|
-
if isinstance(result, Ok):
|
157
|
-
operation = result.ok()
|
158
|
-
definition = cast(GraphQLOperationDefinition, operation.definition)
|
159
|
-
type_name = definition.type_.name if isinstance(definition.type_, GraphQLNamedType) else "Unknown"
|
160
|
-
for_type = output.setdefault(type_name, APIOperationMap(FieldMap()))
|
161
|
-
for_type[definition.field_name] = operation
|
162
|
-
return output
|
163
|
-
|
164
121
|
def get_full_path(self, path: str) -> str:
|
165
122
|
return self.base_path
|
166
123
|
|
167
124
|
@property
|
168
|
-
def
|
169
|
-
return "
|
125
|
+
def specification(self) -> Specification:
|
126
|
+
return Specification.graphql(version="")
|
170
127
|
|
171
128
|
@property
|
172
129
|
def client_schema(self) -> graphql.GraphQLSchema:
|
@@ -183,18 +140,30 @@ class GraphQLSchema(BaseSchema):
|
|
183
140
|
def _get_base_path(self) -> str:
|
184
141
|
return cast(str, urlsplit(self.location).path)
|
185
142
|
|
186
|
-
|
187
|
-
|
143
|
+
def _do_count_operations(self) -> ApiOperationsCount:
|
144
|
+
counter = ApiOperationsCount()
|
188
145
|
raw_schema = self.raw_schema["__schema"]
|
189
|
-
|
146
|
+
dummy_operation = APIOperation(
|
147
|
+
base_url=self.get_base_url(),
|
148
|
+
path=self.base_path,
|
149
|
+
label="",
|
150
|
+
method="POST",
|
151
|
+
schema=self,
|
152
|
+
definition=None, # type: ignore
|
153
|
+
)
|
154
|
+
|
190
155
|
for type_name in ("queryType", "mutationType"):
|
191
156
|
type_def = raw_schema.get(type_name)
|
192
157
|
if type_def is not None:
|
193
158
|
query_type_name = type_def["name"]
|
194
159
|
for type_def in raw_schema.get("types", []):
|
195
160
|
if type_def["name"] == query_type_name:
|
196
|
-
|
197
|
-
|
161
|
+
for field in type_def["fields"]:
|
162
|
+
counter.total += 1
|
163
|
+
dummy_operation.label = f"{query_type_name}.{field['name']}"
|
164
|
+
if not self._should_skip(dummy_operation):
|
165
|
+
counter.selected += 1
|
166
|
+
return counter
|
198
167
|
|
199
168
|
@property
|
200
169
|
def links_count(self) -> int:
|
@@ -202,8 +171,8 @@ class GraphQLSchema(BaseSchema):
|
|
202
171
|
return 0
|
203
172
|
|
204
173
|
def get_all_operations(
|
205
|
-
self,
|
206
|
-
) -> Generator[Result[APIOperation,
|
174
|
+
self, generation_config: GenerationConfig | None = None
|
175
|
+
) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
207
176
|
schema = self.client_schema
|
208
177
|
for root_type, operation_type in (
|
209
178
|
(RootType.QUERY, schema.query_type),
|
@@ -211,114 +180,136 @@ class GraphQLSchema(BaseSchema):
|
|
211
180
|
):
|
212
181
|
if operation_type is None:
|
213
182
|
continue
|
214
|
-
for field_name,
|
215
|
-
operation
|
216
|
-
|
217
|
-
path=self.base_path,
|
218
|
-
verbose_name=f"{operation_type.name}.{field_name}",
|
219
|
-
method="POST",
|
220
|
-
app=self.app,
|
221
|
-
schema=self,
|
222
|
-
# Parameters are not yet supported
|
223
|
-
definition=GraphQLOperationDefinition(
|
224
|
-
raw=definition,
|
225
|
-
resolved=definition,
|
226
|
-
scope="",
|
227
|
-
parameters=[],
|
228
|
-
type_=operation_type,
|
229
|
-
field_name=field_name,
|
230
|
-
root_type=root_type,
|
231
|
-
),
|
232
|
-
case_cls=GraphQLCase,
|
233
|
-
)
|
234
|
-
context = HookContext(operation=operation)
|
235
|
-
if (
|
236
|
-
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
237
|
-
or should_skip_operation(self.hooks, context)
|
238
|
-
or (hooks and should_skip_operation(hooks, context))
|
239
|
-
):
|
183
|
+
for field_name, field_ in operation_type.fields.items():
|
184
|
+
operation = self._build_operation(root_type, operation_type, field_name, field_)
|
185
|
+
if self._should_skip(operation):
|
240
186
|
continue
|
241
187
|
yield Ok(operation)
|
242
188
|
|
189
|
+
def _should_skip(
|
190
|
+
self,
|
191
|
+
operation: APIOperation,
|
192
|
+
_ctx_cache: SimpleNamespace = SimpleNamespace(operation=None),
|
193
|
+
) -> bool:
|
194
|
+
_ctx_cache.operation = operation
|
195
|
+
return not self.filter_set.match(_ctx_cache)
|
196
|
+
|
197
|
+
def _build_operation(
|
198
|
+
self,
|
199
|
+
root_type: RootType,
|
200
|
+
operation_type: graphql.GraphQLObjectType,
|
201
|
+
field_name: str,
|
202
|
+
field: graphql.GraphQlField,
|
203
|
+
) -> APIOperation:
|
204
|
+
return APIOperation(
|
205
|
+
base_url=self.get_base_url(),
|
206
|
+
path=self.base_path,
|
207
|
+
label=f"{operation_type.name}.{field_name}",
|
208
|
+
method="POST",
|
209
|
+
app=self.app,
|
210
|
+
schema=self,
|
211
|
+
# Parameters are not yet supported
|
212
|
+
definition=GraphQLOperationDefinition(
|
213
|
+
raw=field,
|
214
|
+
resolved=field,
|
215
|
+
scope="",
|
216
|
+
type_=operation_type,
|
217
|
+
field_name=field_name,
|
218
|
+
root_type=root_type,
|
219
|
+
),
|
220
|
+
)
|
221
|
+
|
243
222
|
def get_case_strategy(
|
244
223
|
self,
|
245
224
|
operation: APIOperation,
|
246
225
|
hooks: HookDispatcher | None = None,
|
247
226
|
auth_storage: AuthStorage | None = None,
|
248
|
-
|
227
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
249
228
|
generation_config: GenerationConfig | None = None,
|
250
229
|
**kwargs: Any,
|
251
230
|
) -> SearchStrategy:
|
252
|
-
return
|
231
|
+
return graphql_cases(
|
253
232
|
operation=operation,
|
254
|
-
client_schema=self.client_schema,
|
255
233
|
hooks=hooks,
|
256
234
|
auth_storage=auth_storage,
|
257
|
-
|
235
|
+
generation_mode=generation_mode,
|
258
236
|
generation_config=generation_config or self.generation_config,
|
259
237
|
**kwargs,
|
260
238
|
)
|
261
239
|
|
262
|
-
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
263
|
-
return []
|
264
|
-
|
265
|
-
def get_stateful_tests(
|
266
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
267
|
-
) -> Sequence[StatefulTest]:
|
240
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
268
241
|
return []
|
269
242
|
|
270
243
|
def make_case(
|
271
244
|
self,
|
272
245
|
*,
|
273
|
-
case_cls: type[C],
|
274
246
|
operation: APIOperation,
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
247
|
+
method: str | None = None,
|
248
|
+
path: str | None = None,
|
249
|
+
path_parameters: dict[str, Any] | None = None,
|
250
|
+
headers: dict[str, Any] | None = None,
|
251
|
+
cookies: dict[str, Any] | None = None,
|
252
|
+
query: dict[str, Any] | None = None,
|
253
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
280
254
|
media_type: str | None = None,
|
281
|
-
|
282
|
-
|
255
|
+
meta: CaseMetadata | None = None,
|
256
|
+
) -> Case:
|
257
|
+
return Case(
|
283
258
|
operation=operation,
|
259
|
+
method=method or operation.method.upper(),
|
260
|
+
path=path or operation.path,
|
284
261
|
path_parameters=path_parameters,
|
285
262
|
headers=CaseInsensitiveDict(headers) if headers is not None else headers,
|
286
263
|
cookies=cookies,
|
287
264
|
query=query,
|
288
265
|
body=body,
|
289
|
-
media_type=media_type,
|
266
|
+
media_type=media_type or "application/json",
|
267
|
+
meta=meta,
|
290
268
|
)
|
291
269
|
|
292
270
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
293
271
|
return None
|
294
272
|
|
273
|
+
def validate(self) -> None:
|
274
|
+
return None
|
275
|
+
|
295
276
|
|
296
277
|
@dataclass
|
297
|
-
class FieldMap(
|
278
|
+
class FieldMap(Mapping):
|
298
279
|
"""Container for accessing API operations.
|
299
280
|
|
300
281
|
Provides a more specific error message if API operation is not found.
|
301
282
|
"""
|
302
283
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
self.data[key] = value
|
284
|
+
_parent: APIOperationMap
|
285
|
+
_root_type: RootType
|
286
|
+
_operation_type: graphql.GraphQLObjectType
|
307
287
|
|
308
|
-
|
309
|
-
del self.data[key]
|
288
|
+
__slots__ = ("_parent", "_root_type", "_operation_type")
|
310
289
|
|
311
290
|
def __len__(self) -> int:
|
312
|
-
return len(self.
|
291
|
+
return len(self._operation_type.fields)
|
313
292
|
|
314
293
|
def __iter__(self) -> Iterator[str]:
|
315
|
-
return iter(self.
|
294
|
+
return iter(self._operation_type.fields)
|
295
|
+
|
296
|
+
def _init_operation(self, field_name: str) -> APIOperation:
|
297
|
+
schema = cast(GraphQLSchema, self._parent._schema)
|
298
|
+
cache = schema._operation_cache
|
299
|
+
operation = cache.get_operation(field_name)
|
300
|
+
if operation is not None:
|
301
|
+
return operation
|
302
|
+
operation_type = self._operation_type
|
303
|
+
field_ = operation_type.fields[field_name]
|
304
|
+
operation = schema._build_operation(self._root_type, operation_type, field_name, field_)
|
305
|
+
cache.insert_operation(field_name, operation)
|
306
|
+
return operation
|
316
307
|
|
317
308
|
def __getitem__(self, item: str) -> APIOperation:
|
318
309
|
try:
|
319
|
-
return self.
|
310
|
+
return self._init_operation(item)
|
320
311
|
except KeyError as exc:
|
321
|
-
field_names =
|
312
|
+
field_names = list(self._operation_type.fields)
|
322
313
|
matches = get_close_matches(item, field_names)
|
323
314
|
message = f"`{item}` field not found"
|
324
315
|
if matches:
|
@@ -327,48 +318,77 @@ class FieldMap(MutableMapping):
|
|
327
318
|
|
328
319
|
|
329
320
|
@st.composite # type: ignore
|
330
|
-
def
|
321
|
+
def graphql_cases(
|
331
322
|
draw: Callable,
|
323
|
+
*,
|
332
324
|
operation: APIOperation,
|
333
|
-
client_schema: graphql.GraphQLSchema,
|
334
325
|
hooks: HookDispatcher | None = None,
|
335
|
-
auth_storage: AuthStorage | None = None,
|
336
|
-
|
337
|
-
generation_config: GenerationConfig
|
338
|
-
|
326
|
+
auth_storage: auths.AuthStorage | None = None,
|
327
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
328
|
+
generation_config: GenerationConfig,
|
329
|
+
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
330
|
+
headers: NotSet | dict[str, Any] = NOT_SET,
|
331
|
+
cookies: NotSet | dict[str, Any] = NOT_SET,
|
332
|
+
query: NotSet | dict[str, Any] = NOT_SET,
|
333
|
+
body: Any = NOT_SET,
|
334
|
+
media_type: str | None = None,
|
335
|
+
phase: TestPhase = TestPhase.GENERATE,
|
339
336
|
) -> Any:
|
337
|
+
start = time.monotonic()
|
340
338
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
341
339
|
strategy_factory = {
|
342
340
|
RootType.QUERY: gql_st.queries,
|
343
341
|
RootType.MUTATION: gql_st.mutations,
|
344
342
|
}[definition.root_type]
|
345
343
|
hook_context = HookContext(operation)
|
346
|
-
generation_config = generation_config or GenerationConfig()
|
347
344
|
custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
|
348
345
|
strategy = strategy_factory(
|
349
|
-
client_schema,
|
346
|
+
operation.schema.client_schema, # type: ignore[attr-defined]
|
350
347
|
fields=[definition.field_name],
|
351
348
|
custom_scalars=custom_scalars,
|
352
349
|
print_ast=_noop, # type: ignore
|
353
350
|
allow_x00=generation_config.allow_x00,
|
351
|
+
allow_null=generation_config.graphql_allow_null,
|
354
352
|
codec=generation_config.codec,
|
355
353
|
)
|
356
354
|
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
357
355
|
body = draw(strategy)
|
358
356
|
|
359
|
-
path_parameters_ = _generate_parameter("path", draw, operation, hook_context, hooks)
|
360
|
-
headers_ = _generate_parameter("header", draw, operation, hook_context, hooks)
|
361
|
-
cookies_ = _generate_parameter("cookie", draw, operation, hook_context, hooks)
|
362
|
-
query_ = _generate_parameter("query", draw, operation, hook_context, hooks)
|
363
|
-
|
364
|
-
|
357
|
+
path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
|
358
|
+
headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
|
359
|
+
cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
|
360
|
+
query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
|
361
|
+
|
362
|
+
_phase_data = {
|
363
|
+
TestPhase.EXPLICIT: ExplicitPhaseData(),
|
364
|
+
TestPhase.GENERATE: GeneratePhaseData(),
|
365
|
+
}[phase]
|
366
|
+
phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
|
367
|
+
instance = operation.Case(
|
365
368
|
path_parameters=path_parameters_,
|
366
369
|
headers=headers_,
|
367
370
|
cookies=cookies_,
|
368
371
|
query=query_,
|
369
372
|
body=body,
|
370
|
-
|
371
|
-
|
373
|
+
meta=CaseMetadata(
|
374
|
+
generation=GenerationInfo(
|
375
|
+
time=time.monotonic() - start,
|
376
|
+
mode=generation_mode,
|
377
|
+
),
|
378
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
379
|
+
components={
|
380
|
+
kind: ComponentInfo(mode=generation_mode)
|
381
|
+
for kind, value in [
|
382
|
+
(ComponentKind.QUERY, query_),
|
383
|
+
(ComponentKind.PATH_PARAMETERS, path_parameters_),
|
384
|
+
(ComponentKind.HEADERS, headers_),
|
385
|
+
(ComponentKind.COOKIES, cookies_),
|
386
|
+
(ComponentKind.BODY, body),
|
387
|
+
]
|
388
|
+
if value is not NOT_SET
|
389
|
+
},
|
390
|
+
),
|
391
|
+
media_type=media_type or "application/json",
|
372
392
|
) # type: ignore
|
373
393
|
context = auths.AuthContext(
|
374
394
|
operation=operation,
|
@@ -379,11 +399,19 @@ def get_case_strategy(
|
|
379
399
|
|
380
400
|
|
381
401
|
def _generate_parameter(
|
382
|
-
location: str,
|
402
|
+
location: str,
|
403
|
+
explicit: NotSet | dict[str, Any],
|
404
|
+
draw: Callable,
|
405
|
+
operation: APIOperation,
|
406
|
+
context: HookContext,
|
407
|
+
hooks: HookDispatcher | None,
|
383
408
|
) -> Any:
|
384
409
|
# Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
|
385
410
|
container = LOCATION_TO_CONTAINER[location]
|
386
|
-
|
411
|
+
if isinstance(explicit, NotSet):
|
412
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
|
413
|
+
else:
|
414
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
|
387
415
|
return draw(strategy)
|
388
416
|
|
389
417
|
|