schemathesis 3.25.5__py3-none-any.whl → 3.39.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,37 +1,34 @@
|
|
|
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,
|
|
14
|
+
Iterator,
|
|
15
|
+
Mapping,
|
|
16
|
+
NoReturn,
|
|
11
17
|
Sequence,
|
|
12
18
|
TypeVar,
|
|
13
19
|
cast,
|
|
14
|
-
TYPE_CHECKING,
|
|
15
|
-
NoReturn,
|
|
16
|
-
MutableMapping,
|
|
17
|
-
Iterator,
|
|
18
20
|
)
|
|
19
21
|
from urllib.parse import urlsplit, urlunsplit
|
|
20
22
|
|
|
21
23
|
import graphql
|
|
22
|
-
import requests
|
|
23
|
-
from graphql import GraphQLNamedType
|
|
24
24
|
from hypothesis import strategies as st
|
|
25
|
-
from hypothesis.strategies import SearchStrategy
|
|
26
25
|
from hypothesis_graphql import strategies as gql_st
|
|
27
26
|
from requests.structures import CaseInsensitiveDict
|
|
28
27
|
|
|
29
|
-
from ..openapi.constants import LOCATION_TO_CONTAINER
|
|
30
28
|
from ... import auths
|
|
31
|
-
from ...auths import AuthStorage
|
|
32
29
|
from ...checks import not_a_server_error
|
|
33
|
-
from ...constants import NOT_SET
|
|
34
|
-
from ...exceptions import
|
|
30
|
+
from ...constants import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER
|
|
31
|
+
from ...exceptions import OperationNotFound, OperationSchemaError
|
|
35
32
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
36
33
|
from ...hooks import (
|
|
37
34
|
GLOBAL_HOOK_DISPATCHER,
|
|
@@ -41,13 +38,19 @@ from ...hooks import (
|
|
|
41
38
|
should_skip_operation,
|
|
42
39
|
)
|
|
43
40
|
from ...internal.result import Ok, Result
|
|
44
|
-
from ...models import APIOperation, Case,
|
|
45
|
-
from ...schemas import
|
|
46
|
-
from ...stateful import Stateful, StatefulTest
|
|
41
|
+
from ...models import APIOperation, Case, OperationDefinition
|
|
42
|
+
from ...schemas import APIOperationMap, BaseSchema
|
|
47
43
|
from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
|
|
44
|
+
from ..openapi.constants import LOCATION_TO_CONTAINER
|
|
45
|
+
from ._cache import OperationCache
|
|
48
46
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
|
49
47
|
|
|
50
48
|
if TYPE_CHECKING:
|
|
49
|
+
from hypothesis.strategies import SearchStrategy
|
|
50
|
+
|
|
51
|
+
from ...auths import AuthStorage
|
|
52
|
+
from ...internal.checks import CheckFunction
|
|
53
|
+
from ...stateful import Stateful, StatefulTest
|
|
51
54
|
from ...transports.responses import GenericResponse
|
|
52
55
|
|
|
53
56
|
|
|
@@ -59,39 +62,18 @@ class RootType(enum.Enum):
|
|
|
59
62
|
|
|
60
63
|
@dataclass(repr=False)
|
|
61
64
|
class GraphQLCase(Case):
|
|
62
|
-
def
|
|
63
|
-
|
|
65
|
+
def __hash__(self) -> int:
|
|
66
|
+
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
|
67
|
+
|
|
68
|
+
def _get_url(self, base_url: str | None) -> str:
|
|
64
69
|
base_url = self._get_base_url(base_url)
|
|
65
70
|
# Replace the path, in case if the user provided any path parameters via hooks
|
|
66
71
|
parts = list(urlsplit(base_url))
|
|
67
72
|
parts[2] = self.formatted_path
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
73
|
+
return urlunsplit(parts)
|
|
74
|
+
|
|
75
|
+
def _get_body(self) -> Body | NotSet:
|
|
76
|
+
return self.body if isinstance(self.body, (NotSet, bytes)) else {"query": self.body}
|
|
95
77
|
|
|
96
78
|
def validate_response(
|
|
97
79
|
self,
|
|
@@ -100,20 +82,15 @@ class GraphQLCase(Case):
|
|
|
100
82
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
101
83
|
excluded_checks: tuple[CheckFunction, ...] = (),
|
|
102
84
|
code_sample_style: str | None = None,
|
|
85
|
+
headers: dict[str, Any] | None = None,
|
|
86
|
+
transport_kwargs: dict[str, Any] | None = None,
|
|
103
87
|
) -> None:
|
|
104
88
|
checks = checks or (not_a_server_error,)
|
|
105
89
|
checks += additional_checks
|
|
106
90
|
checks = tuple(check for check in checks if check not in excluded_checks)
|
|
107
|
-
return super().validate_response(
|
|
108
|
-
|
|
109
|
-
|
|
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)
|
|
91
|
+
return super().validate_response(
|
|
92
|
+
response, checks, code_sample_style=code_sample_style, headers=headers, transport_kwargs=transport_kwargs
|
|
93
|
+
)
|
|
117
94
|
|
|
118
95
|
|
|
119
96
|
C = TypeVar("C", bound=Case)
|
|
@@ -136,9 +113,37 @@ class GraphQLOperationDefinition(OperationDefinition):
|
|
|
136
113
|
|
|
137
114
|
@dataclass
|
|
138
115
|
class GraphQLSchema(BaseSchema):
|
|
116
|
+
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
|
117
|
+
|
|
139
118
|
def __repr__(self) -> str:
|
|
140
119
|
return f"<{self.__class__.__name__}>"
|
|
141
120
|
|
|
121
|
+
def __iter__(self) -> Iterator[str]:
|
|
122
|
+
schema = self.client_schema
|
|
123
|
+
for operation_type in (
|
|
124
|
+
schema.query_type,
|
|
125
|
+
schema.mutation_type,
|
|
126
|
+
):
|
|
127
|
+
if operation_type is not None:
|
|
128
|
+
yield operation_type.name
|
|
129
|
+
|
|
130
|
+
def _get_operation_map(self, key: str) -> APIOperationMap:
|
|
131
|
+
cache = self._operation_cache
|
|
132
|
+
map = cache.get_map(key)
|
|
133
|
+
if map is not None:
|
|
134
|
+
return map
|
|
135
|
+
schema = self.client_schema
|
|
136
|
+
for root_type, operation_type in (
|
|
137
|
+
(RootType.QUERY, schema.query_type),
|
|
138
|
+
(RootType.MUTATION, schema.mutation_type),
|
|
139
|
+
):
|
|
140
|
+
if operation_type and operation_type.name == key:
|
|
141
|
+
map = APIOperationMap(self, {})
|
|
142
|
+
map._data = FieldMap(map, root_type, operation_type)
|
|
143
|
+
cache.insert_map(key, map)
|
|
144
|
+
return map
|
|
145
|
+
raise KeyError(key)
|
|
146
|
+
|
|
142
147
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
|
143
148
|
raw_schema = self.raw_schema["__schema"]
|
|
144
149
|
type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
|
|
@@ -148,19 +153,6 @@ class GraphQLSchema(BaseSchema):
|
|
|
148
153
|
message += f". Did you mean `{matches[0]}`?"
|
|
149
154
|
raise OperationNotFound(message=message, item=item) from exc
|
|
150
155
|
|
|
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
156
|
def get_full_path(self, path: str) -> str:
|
|
165
157
|
return self.base_path
|
|
166
158
|
|
|
@@ -202,7 +194,7 @@ class GraphQLSchema(BaseSchema):
|
|
|
202
194
|
return 0
|
|
203
195
|
|
|
204
196
|
def get_all_operations(
|
|
205
|
-
self, hooks: HookDispatcher | None = None
|
|
197
|
+
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
|
206
198
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
207
199
|
schema = self.client_schema
|
|
208
200
|
for root_type, operation_type in (
|
|
@@ -211,26 +203,10 @@ class GraphQLSchema(BaseSchema):
|
|
|
211
203
|
):
|
|
212
204
|
if operation_type is None:
|
|
213
205
|
continue
|
|
214
|
-
for field_name,
|
|
215
|
-
operation
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
)
|
|
206
|
+
for field_name, field_ in operation_type.fields.items():
|
|
207
|
+
operation = self._build_operation(root_type, operation_type, field_name, field_)
|
|
208
|
+
if self._should_skip(operation):
|
|
209
|
+
continue
|
|
234
210
|
context = HookContext(operation=operation)
|
|
235
211
|
if (
|
|
236
212
|
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
|
@@ -240,6 +216,40 @@ class GraphQLSchema(BaseSchema):
|
|
|
240
216
|
continue
|
|
241
217
|
yield Ok(operation)
|
|
242
218
|
|
|
219
|
+
def _should_skip(
|
|
220
|
+
self,
|
|
221
|
+
operation: APIOperation,
|
|
222
|
+
_ctx_cache: SimpleNamespace = SimpleNamespace(operation=None),
|
|
223
|
+
) -> bool:
|
|
224
|
+
_ctx_cache.operation = operation
|
|
225
|
+
return not self.filter_set.match(_ctx_cache)
|
|
226
|
+
|
|
227
|
+
def _build_operation(
|
|
228
|
+
self,
|
|
229
|
+
root_type: RootType,
|
|
230
|
+
operation_type: graphql.GraphQLObjectType,
|
|
231
|
+
field_name: str,
|
|
232
|
+
field: graphql.GraphQlField,
|
|
233
|
+
) -> APIOperation:
|
|
234
|
+
return APIOperation(
|
|
235
|
+
base_url=self.get_base_url(),
|
|
236
|
+
path=self.base_path,
|
|
237
|
+
verbose_name=f"{operation_type.name}.{field_name}",
|
|
238
|
+
method="POST",
|
|
239
|
+
app=self.app,
|
|
240
|
+
schema=self,
|
|
241
|
+
# Parameters are not yet supported
|
|
242
|
+
definition=GraphQLOperationDefinition(
|
|
243
|
+
raw=field,
|
|
244
|
+
resolved=field,
|
|
245
|
+
scope="",
|
|
246
|
+
type_=operation_type,
|
|
247
|
+
field_name=field_name,
|
|
248
|
+
root_type=root_type,
|
|
249
|
+
),
|
|
250
|
+
case_cls=GraphQLCase,
|
|
251
|
+
)
|
|
252
|
+
|
|
243
253
|
def get_case_strategy(
|
|
244
254
|
self,
|
|
245
255
|
operation: APIOperation,
|
|
@@ -259,7 +269,9 @@ class GraphQLSchema(BaseSchema):
|
|
|
259
269
|
**kwargs,
|
|
260
270
|
)
|
|
261
271
|
|
|
262
|
-
def get_strategies_from_examples(
|
|
272
|
+
def get_strategies_from_examples(
|
|
273
|
+
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
|
274
|
+
) -> list[SearchStrategy[Case]]:
|
|
263
275
|
return []
|
|
264
276
|
|
|
265
277
|
def get_stateful_tests(
|
|
@@ -286,39 +298,53 @@ class GraphQLSchema(BaseSchema):
|
|
|
286
298
|
cookies=cookies,
|
|
287
299
|
query=query,
|
|
288
300
|
body=body,
|
|
289
|
-
media_type=media_type,
|
|
301
|
+
media_type=media_type or "application/json",
|
|
302
|
+
generation_time=0.0,
|
|
290
303
|
)
|
|
291
304
|
|
|
292
305
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
|
293
306
|
return None
|
|
294
307
|
|
|
308
|
+
def validate(self) -> None:
|
|
309
|
+
return None
|
|
310
|
+
|
|
295
311
|
|
|
296
312
|
@dataclass
|
|
297
|
-
class FieldMap(
|
|
313
|
+
class FieldMap(Mapping):
|
|
298
314
|
"""Container for accessing API operations.
|
|
299
315
|
|
|
300
316
|
Provides a more specific error message if API operation is not found.
|
|
301
317
|
"""
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
self.data[key] = value
|
|
319
|
+
_parent: APIOperationMap
|
|
320
|
+
_root_type: RootType
|
|
321
|
+
_operation_type: graphql.GraphQLObjectType
|
|
307
322
|
|
|
308
|
-
|
|
309
|
-
del self.data[key]
|
|
323
|
+
__slots__ = ("_parent", "_root_type", "_operation_type")
|
|
310
324
|
|
|
311
325
|
def __len__(self) -> int:
|
|
312
|
-
return len(self.
|
|
326
|
+
return len(self._operation_type.fields)
|
|
313
327
|
|
|
314
328
|
def __iter__(self) -> Iterator[str]:
|
|
315
|
-
return iter(self.
|
|
329
|
+
return iter(self._operation_type.fields)
|
|
330
|
+
|
|
331
|
+
def _init_operation(self, field_name: str) -> APIOperation:
|
|
332
|
+
schema = cast(GraphQLSchema, self._parent._schema)
|
|
333
|
+
cache = schema._operation_cache
|
|
334
|
+
operation = cache.get_operation(field_name)
|
|
335
|
+
if operation is not None:
|
|
336
|
+
return operation
|
|
337
|
+
operation_type = self._operation_type
|
|
338
|
+
field_ = operation_type.fields[field_name]
|
|
339
|
+
operation = schema._build_operation(self._root_type, operation_type, field_name, field_)
|
|
340
|
+
cache.insert_operation(field_name, operation)
|
|
341
|
+
return operation
|
|
316
342
|
|
|
317
343
|
def __getitem__(self, item: str) -> APIOperation:
|
|
318
344
|
try:
|
|
319
|
-
return self.
|
|
345
|
+
return self._init_operation(item)
|
|
320
346
|
except KeyError as exc:
|
|
321
|
-
field_names =
|
|
347
|
+
field_names = list(self._operation_type.fields)
|
|
322
348
|
matches = get_close_matches(item, field_names)
|
|
323
349
|
message = f"`{item}` field not found"
|
|
324
350
|
if matches:
|
|
@@ -337,6 +363,7 @@ def get_case_strategy(
|
|
|
337
363
|
generation_config: GenerationConfig | None = None,
|
|
338
364
|
**kwargs: Any,
|
|
339
365
|
) -> Any:
|
|
366
|
+
start = time.monotonic()
|
|
340
367
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
|
341
368
|
strategy_factory = {
|
|
342
369
|
RootType.QUERY: gql_st.queries,
|
|
@@ -351,6 +378,7 @@ def get_case_strategy(
|
|
|
351
378
|
custom_scalars=custom_scalars,
|
|
352
379
|
print_ast=_noop, # type: ignore
|
|
353
380
|
allow_x00=generation_config.allow_x00,
|
|
381
|
+
allow_null=generation_config.graphql_allow_null,
|
|
354
382
|
codec=generation_config.codec,
|
|
355
383
|
)
|
|
356
384
|
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
|
@@ -369,6 +397,8 @@ def get_case_strategy(
|
|
|
369
397
|
body=body,
|
|
370
398
|
operation=operation,
|
|
371
399
|
data_generation_method=data_generation_method,
|
|
400
|
+
generation_time=time.monotonic() - start,
|
|
401
|
+
media_type="application/json",
|
|
372
402
|
) # type: ignore
|
|
373
403
|
context = auths.AuthContext(
|
|
374
404
|
operation=operation,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
from .formats import register_string_format as format
|
|
2
2
|
from .formats import unregister_string_format
|
|
3
3
|
from .loaders import from_aiohttp, from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
|
|
4
|
+
from .media_types import register_media_type as media_type
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Tuple
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ...models import APIOperation
|
|
8
|
+
from ...schemas import APIOperationMap
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class OperationCacheEntry:
|
|
13
|
+
path: str
|
|
14
|
+
method: str
|
|
15
|
+
# The resolution scope of the operation
|
|
16
|
+
scope: str
|
|
17
|
+
# Parent path item
|
|
18
|
+
path_item: dict[str, Any]
|
|
19
|
+
# Unresolved operation definition
|
|
20
|
+
operation: dict[str, Any]
|
|
21
|
+
__slots__ = ("path", "method", "scope", "path_item", "operation")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# During traversal, we need to keep track of the scope, path, and method
|
|
25
|
+
TraversalKey = Tuple[str, str, str]
|
|
26
|
+
OperationId = str
|
|
27
|
+
Reference = str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class OperationCache:
|
|
32
|
+
"""Cache for Open API operations.
|
|
33
|
+
|
|
34
|
+
This cache contains multiple levels to avoid unnecessary parsing of the schema.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Cache to avoid schema traversal on every access
|
|
38
|
+
_id_to_definition: dict[OperationId, OperationCacheEntry] = field(default_factory=dict)
|
|
39
|
+
# Map map between 1st & 2nd level cache keys
|
|
40
|
+
# Even though 1st level keys could be directly mapped to Python objects in memory, we need to keep them separate
|
|
41
|
+
# to ensure a single owner of the operation instance.
|
|
42
|
+
_id_to_operation: dict[OperationId, int] = field(default_factory=dict)
|
|
43
|
+
_traversal_key_to_operation: dict[TraversalKey, int] = field(default_factory=dict)
|
|
44
|
+
_reference_to_operation: dict[Reference, int] = field(default_factory=dict)
|
|
45
|
+
# The actual operations
|
|
46
|
+
_operations: list[APIOperation] = field(default_factory=list)
|
|
47
|
+
# Cache for operation maps
|
|
48
|
+
_maps: dict[str, APIOperationMap] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def known_operation_ids(self) -> list[str]:
|
|
52
|
+
return list(self._id_to_definition)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def has_ids_to_definitions(self) -> bool:
|
|
56
|
+
return bool(self._id_to_definition)
|
|
57
|
+
|
|
58
|
+
def _append_operation(self, operation: APIOperation) -> int:
|
|
59
|
+
idx = len(self._operations)
|
|
60
|
+
self._operations.append(operation)
|
|
61
|
+
return idx
|
|
62
|
+
|
|
63
|
+
def insert_definition_by_id(
|
|
64
|
+
self,
|
|
65
|
+
operation_id: str,
|
|
66
|
+
path: str,
|
|
67
|
+
method: str,
|
|
68
|
+
scope: str,
|
|
69
|
+
path_item: dict[str, Any],
|
|
70
|
+
operation: dict[str, Any],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Insert a new operation definition into cache."""
|
|
73
|
+
self._id_to_definition[operation_id] = OperationCacheEntry(
|
|
74
|
+
path=path, method=method, scope=scope, path_item=path_item, operation=operation
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def get_definition_by_id(self, operation_id: str) -> OperationCacheEntry:
|
|
78
|
+
"""Get an operation definition by its ID."""
|
|
79
|
+
# TODO: Avoid KeyError in the future
|
|
80
|
+
return self._id_to_definition[operation_id]
|
|
81
|
+
|
|
82
|
+
def insert_operation(
|
|
83
|
+
self,
|
|
84
|
+
operation: APIOperation,
|
|
85
|
+
*,
|
|
86
|
+
traversal_key: TraversalKey,
|
|
87
|
+
operation_id: str | None = None,
|
|
88
|
+
reference: str | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Insert a new operation into cache by one or multiple keys."""
|
|
91
|
+
idx = self._append_operation(operation)
|
|
92
|
+
self._traversal_key_to_operation[traversal_key] = idx
|
|
93
|
+
if operation_id is not None:
|
|
94
|
+
self._id_to_operation[operation_id] = idx
|
|
95
|
+
if reference is not None:
|
|
96
|
+
self._reference_to_operation[reference] = idx
|
|
97
|
+
|
|
98
|
+
def get_operation_by_id(self, operation_id: str) -> APIOperation | None:
|
|
99
|
+
"""Get an operation by its ID."""
|
|
100
|
+
idx = self._id_to_operation.get(operation_id)
|
|
101
|
+
if idx is not None:
|
|
102
|
+
return self._operations[idx]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def get_operation_by_reference(self, reference: str) -> APIOperation | None:
|
|
106
|
+
"""Get an operation by its reference."""
|
|
107
|
+
idx = self._reference_to_operation.get(reference)
|
|
108
|
+
if idx is not None:
|
|
109
|
+
return self._operations[idx]
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def get_operation_by_traversal_key(self, key: TraversalKey) -> APIOperation | None:
|
|
113
|
+
"""Get an operation by its traverse key."""
|
|
114
|
+
idx = self._traversal_key_to_operation.get(key)
|
|
115
|
+
if idx is not None:
|
|
116
|
+
return self._operations[idx]
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def get_map(self, key: str) -> APIOperationMap | None:
|
|
120
|
+
return self._maps.get(key)
|
|
121
|
+
|
|
122
|
+
def insert_map(self, key: str, value: APIOperationMap) -> None:
|
|
123
|
+
self._maps[key] = value
|