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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
from ...exceptions import UsageError
|
|
@@ -8,7 +10,7 @@ if TYPE_CHECKING:
|
|
|
8
10
|
import graphql
|
|
9
11
|
from hypothesis import strategies as st
|
|
10
12
|
|
|
11
|
-
CUSTOM_SCALARS:
|
|
13
|
+
CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
@@ -24,3 +26,25 @@ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
|
24
26
|
if not isinstance(strategy, SearchStrategy):
|
|
25
27
|
raise UsageError(f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar")
|
|
26
28
|
CUSTOM_SCALARS[name] = strategy
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@lru_cache
|
|
32
|
+
def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
|
|
33
|
+
"""Get all extra GraphQL strategies."""
|
|
34
|
+
from . import nodes
|
|
35
|
+
from hypothesis import strategies as st
|
|
36
|
+
|
|
37
|
+
dates = st.dates().map(str)
|
|
38
|
+
times = st.times().map("%sZ".__mod__)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
"Date": dates.map(nodes.String),
|
|
42
|
+
"Time": times.map(nodes.String),
|
|
43
|
+
"DateTime": st.tuples(dates, times).map("T".join).map(nodes.String),
|
|
44
|
+
"IP": st.ip_addresses().map(str).map(nodes.String),
|
|
45
|
+
"IPv4": st.ip_addresses(v=4).map(str).map(nodes.String),
|
|
46
|
+
"IPv6": st.ip_addresses(v=6).map(str).map(nodes.String),
|
|
47
|
+
"BigInt": st.integers().map(nodes.Int),
|
|
48
|
+
"Long": st.integers(min_value=-(2**63), max_value=2**63 - 1).map(nodes.Int),
|
|
49
|
+
"UUID": st.uuids().map(str).map(nodes.String),
|
|
50
|
+
}
|
|
@@ -5,15 +5,9 @@ from enum import unique
|
|
|
5
5
|
from typing import (
|
|
6
6
|
Any,
|
|
7
7
|
Callable,
|
|
8
|
-
Dict,
|
|
9
8
|
Generator,
|
|
10
|
-
List,
|
|
11
|
-
Optional,
|
|
12
9
|
Sequence,
|
|
13
|
-
Tuple,
|
|
14
|
-
Type,
|
|
15
10
|
TypeVar,
|
|
16
|
-
Union,
|
|
17
11
|
cast,
|
|
18
12
|
TYPE_CHECKING,
|
|
19
13
|
)
|
|
@@ -29,7 +23,7 @@ from requests.structures import CaseInsensitiveDict
|
|
|
29
23
|
from ... import auths
|
|
30
24
|
from ...auths import AuthStorage
|
|
31
25
|
from ...checks import not_a_server_error
|
|
32
|
-
from ...generation import DataGenerationMethod
|
|
26
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
33
27
|
from ...exceptions import OperationSchemaError
|
|
34
28
|
from ...constants import NOT_SET
|
|
35
29
|
from ...hooks import (
|
|
@@ -44,7 +38,7 @@ from ...models import APIOperation, Case, CheckFunction, OperationDefinition
|
|
|
44
38
|
from ...schemas import BaseSchema
|
|
45
39
|
from ...stateful import Stateful, StatefulTest
|
|
46
40
|
from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
|
|
47
|
-
from .scalars import CUSTOM_SCALARS
|
|
41
|
+
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
|
48
42
|
|
|
49
43
|
if TYPE_CHECKING:
|
|
50
44
|
from ...transports.responses import GenericResponse
|
|
@@ -58,12 +52,10 @@ class RootType(enum.Enum):
|
|
|
58
52
|
|
|
59
53
|
@dataclass(repr=False)
|
|
60
54
|
class GraphQLCase(Case):
|
|
61
|
-
def as_requests_kwargs(
|
|
62
|
-
self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
|
|
63
|
-
) -> Dict[str, Any]:
|
|
55
|
+
def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
|
64
56
|
final_headers = self._get_headers(headers)
|
|
65
57
|
base_url = self._get_base_url(base_url)
|
|
66
|
-
kwargs:
|
|
58
|
+
kwargs: dict[str, Any] = {"method": self.method, "url": base_url, "headers": final_headers}
|
|
67
59
|
# There is no direct way to have bytes here, but it is a useful pattern to support.
|
|
68
60
|
# It also unifies GraphQLCase with its Open API counterpart where bytes may come from external examples
|
|
69
61
|
if isinstance(self.body, bytes):
|
|
@@ -74,7 +66,7 @@ class GraphQLCase(Case):
|
|
|
74
66
|
kwargs["json"] = {"query": self.body}
|
|
75
67
|
return kwargs
|
|
76
68
|
|
|
77
|
-
def as_werkzeug_kwargs(self, headers:
|
|
69
|
+
def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
|
78
70
|
final_headers = self._get_headers(headers)
|
|
79
71
|
return {
|
|
80
72
|
"method": self.method,
|
|
@@ -88,10 +80,10 @@ class GraphQLCase(Case):
|
|
|
88
80
|
def validate_response(
|
|
89
81
|
self,
|
|
90
82
|
response: GenericResponse,
|
|
91
|
-
checks:
|
|
92
|
-
additional_checks:
|
|
93
|
-
excluded_checks:
|
|
94
|
-
code_sample_style:
|
|
83
|
+
checks: tuple[CheckFunction, ...] = (),
|
|
84
|
+
additional_checks: tuple[CheckFunction, ...] = (),
|
|
85
|
+
excluded_checks: tuple[CheckFunction, ...] = (),
|
|
86
|
+
code_sample_style: str | None = None,
|
|
95
87
|
) -> None:
|
|
96
88
|
checks = checks or (not_a_server_error,)
|
|
97
89
|
checks += additional_checks
|
|
@@ -101,8 +93,8 @@ class GraphQLCase(Case):
|
|
|
101
93
|
def call_asgi(
|
|
102
94
|
self,
|
|
103
95
|
app: Any = None,
|
|
104
|
-
base_url:
|
|
105
|
-
headers:
|
|
96
|
+
base_url: str | None = None,
|
|
97
|
+
headers: dict[str, str] | None = None,
|
|
106
98
|
**kwargs: Any,
|
|
107
99
|
) -> requests.Response:
|
|
108
100
|
return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)
|
|
@@ -163,8 +155,13 @@ class GraphQLSchema(BaseSchema):
|
|
|
163
155
|
total += len(type_def["fields"])
|
|
164
156
|
return total
|
|
165
157
|
|
|
158
|
+
@property
|
|
159
|
+
def links_count(self) -> int:
|
|
160
|
+
# Links are not supported for GraphQL
|
|
161
|
+
return 0
|
|
162
|
+
|
|
166
163
|
def get_all_operations(
|
|
167
|
-
self, hooks:
|
|
164
|
+
self, hooks: HookDispatcher | None = None
|
|
168
165
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
169
166
|
schema = self.client_schema
|
|
170
167
|
for root_type, operation_type in (
|
|
@@ -205,9 +202,10 @@ class GraphQLSchema(BaseSchema):
|
|
|
205
202
|
def get_case_strategy(
|
|
206
203
|
self,
|
|
207
204
|
operation: APIOperation,
|
|
208
|
-
hooks:
|
|
209
|
-
auth_storage:
|
|
205
|
+
hooks: HookDispatcher | None = None,
|
|
206
|
+
auth_storage: AuthStorage | None = None,
|
|
210
207
|
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
|
208
|
+
generation_config: GenerationConfig | None = None,
|
|
211
209
|
**kwargs: Any,
|
|
212
210
|
) -> SearchStrategy:
|
|
213
211
|
return get_case_strategy(
|
|
@@ -216,28 +214,29 @@ class GraphQLSchema(BaseSchema):
|
|
|
216
214
|
hooks=hooks,
|
|
217
215
|
auth_storage=auth_storage,
|
|
218
216
|
data_generation_method=data_generation_method,
|
|
217
|
+
generation_config=generation_config,
|
|
219
218
|
**kwargs,
|
|
220
219
|
)
|
|
221
220
|
|
|
222
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
221
|
+
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
|
223
222
|
return []
|
|
224
223
|
|
|
225
224
|
def get_stateful_tests(
|
|
226
|
-
self, response: GenericResponse, operation: APIOperation, stateful:
|
|
225
|
+
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
|
227
226
|
) -> Sequence[StatefulTest]:
|
|
228
227
|
return []
|
|
229
228
|
|
|
230
229
|
def make_case(
|
|
231
230
|
self,
|
|
232
231
|
*,
|
|
233
|
-
case_cls:
|
|
232
|
+
case_cls: type[C],
|
|
234
233
|
operation: APIOperation,
|
|
235
|
-
path_parameters:
|
|
236
|
-
headers:
|
|
237
|
-
cookies:
|
|
238
|
-
query:
|
|
239
|
-
body:
|
|
240
|
-
media_type:
|
|
234
|
+
path_parameters: PathParameters | None = None,
|
|
235
|
+
headers: Headers | None = None,
|
|
236
|
+
cookies: Cookies | None = None,
|
|
237
|
+
query: Query | None = None,
|
|
238
|
+
body: Body | NotSet = NOT_SET,
|
|
239
|
+
media_type: str | None = None,
|
|
241
240
|
) -> C:
|
|
242
241
|
return case_cls(
|
|
243
242
|
operation=operation,
|
|
@@ -255,9 +254,10 @@ def get_case_strategy(
|
|
|
255
254
|
draw: Callable,
|
|
256
255
|
operation: APIOperation,
|
|
257
256
|
client_schema: graphql.GraphQLSchema,
|
|
258
|
-
hooks:
|
|
259
|
-
auth_storage:
|
|
257
|
+
hooks: HookDispatcher | None = None,
|
|
258
|
+
auth_storage: AuthStorage | None = None,
|
|
260
259
|
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
|
260
|
+
generation_config: GenerationConfig | None = None,
|
|
261
261
|
**kwargs: Any,
|
|
262
262
|
) -> Any:
|
|
263
263
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
|
@@ -266,11 +266,15 @@ def get_case_strategy(
|
|
|
266
266
|
RootType.MUTATION: gql_st.mutations,
|
|
267
267
|
}[definition.root_type]
|
|
268
268
|
hook_context = HookContext(operation)
|
|
269
|
+
generation_config = generation_config or GenerationConfig()
|
|
270
|
+
custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
|
|
269
271
|
strategy = strategy_factory(
|
|
270
272
|
client_schema,
|
|
271
273
|
fields=[definition.field_name],
|
|
272
|
-
custom_scalars=
|
|
274
|
+
custom_scalars=custom_scalars,
|
|
273
275
|
print_ast=_noop, # type: ignore
|
|
276
|
+
allow_x00=generation_config.allow_x00,
|
|
277
|
+
codec=generation_config.codec,
|
|
274
278
|
)
|
|
275
279
|
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
|
276
280
|
body = draw(strategy)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import re
|
|
2
3
|
import string
|
|
3
4
|
from base64 import b64encode
|
|
4
5
|
from contextlib import suppress
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from functools import lru_cache
|
|
7
|
-
from typing import Any, Callable, Dict, Iterable,
|
|
8
|
+
from typing import Any, Callable, Dict, Iterable, Optional
|
|
8
9
|
from urllib.parse import quote_plus
|
|
9
10
|
from weakref import WeakKeyDictionary
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ from requests.structures import CaseInsensitiveDict
|
|
|
16
17
|
from ...constants import NOT_SET
|
|
17
18
|
from .formats import STRING_FORMATS
|
|
18
19
|
from ... import auths, serializers
|
|
19
|
-
from ...generation import DataGenerationMethod
|
|
20
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
20
21
|
from ...internal.copy import fast_deepcopy
|
|
21
22
|
from ...exceptions import SerializationNotPossible, BodyInGetRequestError
|
|
22
23
|
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
|
@@ -34,14 +35,14 @@ from .utils import is_header_location
|
|
|
34
35
|
HEADER_FORMAT = "_header_value"
|
|
35
36
|
PARAMETERS = frozenset(("path_parameters", "headers", "cookies", "query", "body"))
|
|
36
37
|
SLASH = "/"
|
|
37
|
-
StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str]], st.SearchStrategy]
|
|
38
|
+
StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
@lru_cache
|
|
41
|
-
def get_default_format_strategies() ->
|
|
41
|
+
@lru_cache
|
|
42
|
+
def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
|
|
42
43
|
"""Get all default "format" strategies."""
|
|
43
44
|
|
|
44
|
-
def make_basic_auth_str(item:
|
|
45
|
+
def make_basic_auth_str(item: tuple[str, str]) -> str:
|
|
45
46
|
return _basic_auth_str(*item)
|
|
46
47
|
|
|
47
48
|
latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
|
|
@@ -63,7 +64,7 @@ def get_default_format_strategies() -> Dict[str, st.SearchStrategy]:
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def is_valid_header(headers:
|
|
67
|
+
def is_valid_header(headers: dict[str, Any]) -> bool:
|
|
67
68
|
"""Verify if the generated headers are valid."""
|
|
68
69
|
for name, value in headers.items():
|
|
69
70
|
if not is_latin_1_encodable(value):
|
|
@@ -83,7 +84,7 @@ def is_illegal_surrogate(item: Any) -> bool:
|
|
|
83
84
|
return isinstance(item, str) and bool(has_surrogate_pair(item))
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def is_valid_query(query:
|
|
87
|
+
def is_valid_query(query: dict[str, Any]) -> bool:
|
|
87
88
|
"""Surrogates are not allowed in a query string.
|
|
88
89
|
|
|
89
90
|
`requests` and `werkzeug` will fail to send it to the application.
|
|
@@ -98,13 +99,14 @@ def is_valid_query(query: Dict[str, Any]) -> bool:
|
|
|
98
99
|
def get_case_strategy(
|
|
99
100
|
draw: Callable,
|
|
100
101
|
operation: APIOperation,
|
|
101
|
-
hooks:
|
|
102
|
-
auth_storage:
|
|
102
|
+
hooks: HookDispatcher | None = None,
|
|
103
|
+
auth_storage: auths.AuthStorage | None = None,
|
|
103
104
|
generator: DataGenerationMethod = DataGenerationMethod.default(),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
generation_config: GenerationConfig | None = None,
|
|
106
|
+
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
|
107
|
+
headers: NotSet | dict[str, Any] = NOT_SET,
|
|
108
|
+
cookies: NotSet | dict[str, Any] = NOT_SET,
|
|
109
|
+
query: NotSet | dict[str, Any] = NOT_SET,
|
|
108
110
|
body: Any = NOT_SET,
|
|
109
111
|
) -> Any:
|
|
110
112
|
"""A strategy that creates `Case` instances.
|
|
@@ -123,10 +125,14 @@ def get_case_strategy(
|
|
|
123
125
|
|
|
124
126
|
context = HookContext(operation)
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
generation_config = generation_config or GenerationConfig()
|
|
129
|
+
|
|
130
|
+
path_parameters_ = generate_parameter(
|
|
131
|
+
"path", path_parameters, operation, draw, context, hooks, generator, generation_config
|
|
132
|
+
)
|
|
133
|
+
headers_ = generate_parameter("header", headers, operation, draw, context, hooks, generator, generation_config)
|
|
134
|
+
cookies_ = generate_parameter("cookie", cookies, operation, draw, context, hooks, generator, generation_config)
|
|
135
|
+
query_ = generate_parameter("query", query, operation, draw, context, hooks, generator, generation_config)
|
|
130
136
|
|
|
131
137
|
media_type = None
|
|
132
138
|
if body is NOT_SET:
|
|
@@ -143,7 +149,7 @@ def get_case_strategy(
|
|
|
143
149
|
else:
|
|
144
150
|
candidates = operation.body.items
|
|
145
151
|
parameter = draw(st.sampled_from(candidates))
|
|
146
|
-
strategy = _get_body_strategy(parameter, strategy_factory, operation)
|
|
152
|
+
strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
|
|
147
153
|
strategy = apply_hooks(operation, context, hooks, strategy, "body")
|
|
148
154
|
# Parameter may have a wildcard media type. In this case, choose any supported one
|
|
149
155
|
possible_media_types = sorted(serializers.get_matching_media_types(parameter.media_type))
|
|
@@ -198,6 +204,7 @@ def _get_body_strategy(
|
|
|
198
204
|
parameter: OpenAPIBody,
|
|
199
205
|
strategy_factory: StrategyFactory,
|
|
200
206
|
operation: APIOperation,
|
|
207
|
+
generation_config: GenerationConfig,
|
|
201
208
|
) -> st.SearchStrategy:
|
|
202
209
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
|
203
210
|
# Note, the parent schema is not included as each parameter belong only to one schema
|
|
@@ -205,7 +212,7 @@ def _get_body_strategy(
|
|
|
205
212
|
return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
|
|
206
213
|
schema = parameter.as_json_schema(operation)
|
|
207
214
|
schema = operation.schema.prepare_schema(schema)
|
|
208
|
-
strategy = strategy_factory(schema, operation.verbose_name, "body", parameter.media_type)
|
|
215
|
+
strategy = strategy_factory(schema, operation.verbose_name, "body", parameter.media_type, generation_config)
|
|
209
216
|
if not parameter.is_required:
|
|
210
217
|
strategy |= st.just(NOT_SET)
|
|
211
218
|
_BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
|
|
@@ -213,24 +220,25 @@ def _get_body_strategy(
|
|
|
213
220
|
|
|
214
221
|
|
|
215
222
|
def get_parameters_value(
|
|
216
|
-
value:
|
|
223
|
+
value: NotSet | dict[str, Any],
|
|
217
224
|
location: str,
|
|
218
225
|
draw: Callable,
|
|
219
226
|
operation: APIOperation,
|
|
220
227
|
context: HookContext,
|
|
221
|
-
hooks:
|
|
228
|
+
hooks: HookDispatcher | None,
|
|
222
229
|
strategy_factory: StrategyFactory,
|
|
223
|
-
|
|
230
|
+
generation_config: GenerationConfig,
|
|
231
|
+
) -> dict[str, Any] | None:
|
|
224
232
|
"""Get the final value for the specified location.
|
|
225
233
|
|
|
226
234
|
If the value is not set, then generate it from the relevant strategy. Otherwise, check what is missing in it and
|
|
227
235
|
generate those parts.
|
|
228
236
|
"""
|
|
229
237
|
if isinstance(value, NotSet) or not value:
|
|
230
|
-
strategy = get_parameters_strategy(operation, strategy_factory, location)
|
|
238
|
+
strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config)
|
|
231
239
|
strategy = apply_hooks(operation, context, hooks, strategy, location)
|
|
232
240
|
return draw(strategy)
|
|
233
|
-
strategy = get_parameters_strategy(operation, strategy_factory, location, exclude=value.keys())
|
|
241
|
+
strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config, exclude=value.keys())
|
|
234
242
|
strategy = apply_hooks(operation, context, hooks, strategy, location)
|
|
235
243
|
new = draw(strategy)
|
|
236
244
|
if new is not None:
|
|
@@ -249,7 +257,7 @@ class ValueContainer:
|
|
|
249
257
|
|
|
250
258
|
value: Any
|
|
251
259
|
location: str
|
|
252
|
-
generator:
|
|
260
|
+
generator: DataGenerationMethod | None
|
|
253
261
|
|
|
254
262
|
@property
|
|
255
263
|
def is_generated(self) -> bool:
|
|
@@ -257,19 +265,20 @@ class ValueContainer:
|
|
|
257
265
|
return self.generator is not None and (self.location == "body" or self.value is not None)
|
|
258
266
|
|
|
259
267
|
|
|
260
|
-
def any_negated_values(values:
|
|
268
|
+
def any_negated_values(values: list[ValueContainer]) -> bool:
|
|
261
269
|
"""Check if any generated values are negated."""
|
|
262
270
|
return any(value.generator == DataGenerationMethod.negative for value in values if value.is_generated)
|
|
263
271
|
|
|
264
272
|
|
|
265
273
|
def generate_parameter(
|
|
266
274
|
location: str,
|
|
267
|
-
explicit:
|
|
275
|
+
explicit: NotSet | dict[str, Any],
|
|
268
276
|
operation: APIOperation,
|
|
269
277
|
draw: Callable,
|
|
270
278
|
context: HookContext,
|
|
271
|
-
hooks:
|
|
279
|
+
hooks: HookDispatcher | None,
|
|
272
280
|
generator: DataGenerationMethod,
|
|
281
|
+
generation_config: GenerationConfig,
|
|
273
282
|
) -> ValueContainer:
|
|
274
283
|
"""Generate a value for a parameter.
|
|
275
284
|
|
|
@@ -285,8 +294,10 @@ def generate_parameter(
|
|
|
285
294
|
generator = DataGenerationMethod.positive
|
|
286
295
|
else:
|
|
287
296
|
strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
|
|
288
|
-
value = get_parameters_value(
|
|
289
|
-
|
|
297
|
+
value = get_parameters_value(
|
|
298
|
+
explicit, location, draw, operation, context, hooks, strategy_factory, generation_config
|
|
299
|
+
)
|
|
300
|
+
used_generator: DataGenerationMethod | None = generator
|
|
290
301
|
if value == explicit:
|
|
291
302
|
# When we pass `explicit`, then its parts are excluded from generation of the final value
|
|
292
303
|
# If the final value is the same, then other parameters were generated at all
|
|
@@ -319,6 +330,7 @@ def get_parameters_strategy(
|
|
|
319
330
|
operation: APIOperation,
|
|
320
331
|
strategy_factory: StrategyFactory,
|
|
321
332
|
location: str,
|
|
333
|
+
generation_config: GenerationConfig,
|
|
322
334
|
exclude: Iterable[str] = (),
|
|
323
335
|
) -> st.SearchStrategy:
|
|
324
336
|
"""Create a new strategy for the case's component from the API operation parameters."""
|
|
@@ -345,7 +357,7 @@ def get_parameters_strategy(
|
|
|
345
357
|
# Nothing to negate - all properties were excluded
|
|
346
358
|
strategy = st.none()
|
|
347
359
|
else:
|
|
348
|
-
strategy = strategy_factory(schema, operation.verbose_name, location, None)
|
|
360
|
+
strategy = strategy_factory(schema, operation.verbose_name, location, None, generation_config)
|
|
349
361
|
serialize = operation.get_parameter_serializer(location)
|
|
350
362
|
if serialize is not None:
|
|
351
363
|
strategy = strategy.map(serialize)
|
|
@@ -374,7 +386,7 @@ def get_parameters_strategy(
|
|
|
374
386
|
return st.none()
|
|
375
387
|
|
|
376
388
|
|
|
377
|
-
def jsonify_python_specific_types(value:
|
|
389
|
+
def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
|
|
378
390
|
"""Convert Python-specific values to their JSON equivalents."""
|
|
379
391
|
stack: list = [value]
|
|
380
392
|
while stack:
|
|
@@ -395,11 +407,12 @@ def jsonify_python_specific_types(value: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
395
407
|
|
|
396
408
|
|
|
397
409
|
def make_positive_strategy(
|
|
398
|
-
schema:
|
|
410
|
+
schema: dict[str, Any],
|
|
399
411
|
operation_name: str,
|
|
400
412
|
location: str,
|
|
401
|
-
media_type:
|
|
402
|
-
|
|
413
|
+
media_type: str | None,
|
|
414
|
+
generation_config: GenerationConfig,
|
|
415
|
+
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
|
403
416
|
) -> st.SearchStrategy:
|
|
404
417
|
"""Strategy for generating values that fit the schema."""
|
|
405
418
|
if is_header_location(location):
|
|
@@ -410,21 +423,25 @@ def make_positive_strategy(
|
|
|
410
423
|
if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
|
|
411
424
|
sub_schema.setdefault("format", HEADER_FORMAT)
|
|
412
425
|
return from_schema(
|
|
413
|
-
schema,
|
|
426
|
+
schema,
|
|
427
|
+
custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
|
|
428
|
+
allow_x00=generation_config.allow_x00,
|
|
429
|
+
codec=generation_config.codec,
|
|
414
430
|
)
|
|
415
431
|
|
|
416
432
|
|
|
417
|
-
def _can_skip_header_filter(schema:
|
|
433
|
+
def _can_skip_header_filter(schema: dict[str, Any]) -> bool:
|
|
418
434
|
# All headers should contain HEADER_FORMAT in order to avoid header filter
|
|
419
435
|
return all(sub_schema.get("format") == HEADER_FORMAT for sub_schema in schema.get("properties", {}).values())
|
|
420
436
|
|
|
421
437
|
|
|
422
438
|
def make_negative_strategy(
|
|
423
|
-
schema:
|
|
439
|
+
schema: dict[str, Any],
|
|
424
440
|
operation_name: str,
|
|
425
441
|
location: str,
|
|
426
|
-
media_type:
|
|
427
|
-
|
|
442
|
+
media_type: str | None,
|
|
443
|
+
generation_config: GenerationConfig,
|
|
444
|
+
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
|
428
445
|
) -> st.SearchStrategy:
|
|
429
446
|
return negative_schema(
|
|
430
447
|
schema,
|
|
@@ -432,6 +449,7 @@ def make_negative_strategy(
|
|
|
432
449
|
location=location,
|
|
433
450
|
media_type=media_type,
|
|
434
451
|
custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
|
|
452
|
+
generation_config=generation_config,
|
|
435
453
|
)
|
|
436
454
|
|
|
437
455
|
|
|
@@ -441,7 +459,7 @@ DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY = {
|
|
|
441
459
|
}
|
|
442
460
|
|
|
443
461
|
|
|
444
|
-
def is_valid_path(parameters:
|
|
462
|
+
def is_valid_path(parameters: dict[str, Any]) -> bool:
|
|
445
463
|
"""Empty strings ("") are excluded from path by urllib3.
|
|
446
464
|
|
|
447
465
|
A path containing to "/" or "%2F" will lead to ambiguous path resolution in
|
|
@@ -459,7 +477,7 @@ def is_valid_path(parameters: Dict[str, Any]) -> bool:
|
|
|
459
477
|
)
|
|
460
478
|
|
|
461
479
|
|
|
462
|
-
def quote_all(parameters:
|
|
480
|
+
def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
463
481
|
"""Apply URL quotation for all values in a dictionary."""
|
|
464
482
|
# Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
|
|
465
483
|
# It will change the path:
|
|
@@ -481,7 +499,7 @@ def quote_all(parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
481
499
|
def apply_hooks(
|
|
482
500
|
operation: APIOperation,
|
|
483
501
|
context: HookContext,
|
|
484
|
-
hooks:
|
|
502
|
+
hooks: HookDispatcher | None,
|
|
485
503
|
strategy: st.SearchStrategy,
|
|
486
504
|
location: str,
|
|
487
505
|
) -> st.SearchStrategy:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TYPE_CHECKING, Any,
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Generator, NoReturn
|
|
3
3
|
|
|
4
4
|
from ... import failures
|
|
5
5
|
from ...exceptions import (
|
|
@@ -17,11 +17,11 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from ...models import Case
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def status_code_conformance(response: GenericResponse, case:
|
|
20
|
+
def status_code_conformance(response: GenericResponse, case: Case) -> bool | None:
|
|
21
21
|
from .schemas import BaseOpenAPISchema
|
|
22
22
|
|
|
23
23
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
24
|
-
|
|
24
|
+
return True
|
|
25
25
|
responses = case.operation.definition.raw.get("responses", {})
|
|
26
26
|
# "default" can be used as the default response object for all HTTP codes that are not covered individually
|
|
27
27
|
if "default" in responses:
|
|
@@ -43,16 +43,16 @@ def status_code_conformance(response: GenericResponse, case: "Case") -> Optional
|
|
|
43
43
|
return None # explicitly return None for mypy
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _expand_responses(responses:
|
|
46
|
+
def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, None]:
|
|
47
47
|
for code in responses:
|
|
48
48
|
yield from expand_status_code(code)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def content_type_conformance(response: GenericResponse, case:
|
|
51
|
+
def content_type_conformance(response: GenericResponse, case: Case) -> bool | None:
|
|
52
52
|
from .schemas import BaseOpenAPISchema
|
|
53
53
|
|
|
54
54
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
55
|
-
|
|
55
|
+
return True
|
|
56
56
|
documented_content_types = case.operation.schema.get_content_types(case.operation, response)
|
|
57
57
|
if not documented_content_types:
|
|
58
58
|
return None
|
|
@@ -96,11 +96,11 @@ def _reraise_malformed_media_type(exc: ValueError, location: str, actual: str, d
|
|
|
96
96
|
) from exc
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def response_headers_conformance(response: GenericResponse, case:
|
|
99
|
+
def response_headers_conformance(response: GenericResponse, case: Case) -> bool | None:
|
|
100
100
|
from .schemas import BaseOpenAPISchema
|
|
101
101
|
|
|
102
102
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
103
|
-
|
|
103
|
+
return True
|
|
104
104
|
defined_headers = case.operation.schema.get_headers(case.operation, response)
|
|
105
105
|
if not defined_headers:
|
|
106
106
|
return None
|
|
@@ -121,9 +121,9 @@ def response_headers_conformance(response: GenericResponse, case: "Case") -> Opt
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
def response_schema_conformance(response: GenericResponse, case:
|
|
124
|
+
def response_schema_conformance(response: GenericResponse, case: Case) -> bool | None:
|
|
125
125
|
from .schemas import BaseOpenAPISchema
|
|
126
126
|
|
|
127
127
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
128
|
-
|
|
128
|
+
return True
|
|
129
129
|
return case.operation.validate_response(response)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from itertools import chain
|
|
2
|
-
from typing import Any, Callable
|
|
3
|
+
from typing import Any, Callable
|
|
3
4
|
|
|
4
5
|
from ...internal.jsonschema import traverse_schema
|
|
5
6
|
from ...internal.copy import fast_deepcopy
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def to_json_schema(
|
|
9
|
-
schema:
|
|
10
|
-
) ->
|
|
10
|
+
schema: dict[str, Any], *, nullable_name: str, copy: bool = True, is_response_schema: bool = False
|
|
11
|
+
) -> dict[str, Any]:
|
|
11
12
|
"""Convert Open API parameters to JSON Schema.
|
|
12
13
|
|
|
13
14
|
NOTE. This function is applied to all keywords (including nested) during a schema resolving, thus it is not recursive.
|
|
@@ -32,7 +33,7 @@ def to_json_schema(
|
|
|
32
33
|
return schema
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def rewrite_properties(schema:
|
|
36
|
+
def rewrite_properties(schema: dict[str, Any], predicate: Callable[[dict[str, Any]], bool]) -> None:
|
|
36
37
|
required = schema.get("required", [])
|
|
37
38
|
forbidden = []
|
|
38
39
|
for name, subschema in list(schema.get("properties", {}).items()):
|
|
@@ -49,7 +50,7 @@ def rewrite_properties(schema: Dict[str, Any], predicate: Callable[[Dict[str, An
|
|
|
49
50
|
schema.pop("properties", None)
|
|
50
51
|
|
|
51
52
|
|
|
52
|
-
def forbid_properties(schema:
|
|
53
|
+
def forbid_properties(schema: dict[str, Any], forbidden: list[str]) -> None:
|
|
53
54
|
"""Explicitly forbid properties via the `not` keyword."""
|
|
54
55
|
not_schema = schema.setdefault("not", {})
|
|
55
56
|
already_forbidden = not_schema.setdefault("required", [])
|
|
@@ -57,15 +58,15 @@ def forbid_properties(schema: Dict[str, Any], forbidden: List[str]) -> None:
|
|
|
57
58
|
not_schema["required"] = list(set(chain(already_forbidden, forbidden)))
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def is_write_only(schema:
|
|
61
|
+
def is_write_only(schema: dict[str, Any]) -> bool:
|
|
61
62
|
return schema.get("writeOnly", False) or schema.get("x-writeOnly", False)
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def is_read_only(schema:
|
|
65
|
+
def is_read_only(schema: dict[str, Any]) -> bool:
|
|
65
66
|
return schema.get("readOnly", False)
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def to_json_schema_recursive(
|
|
69
|
-
schema:
|
|
70
|
-
) ->
|
|
70
|
+
schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False
|
|
71
|
+
) -> dict[str, Any]:
|
|
71
72
|
return traverse_schema(schema, to_json_schema, nullable_name=nullable_name, is_response_schema=is_response_schema)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# These schemas are copied from https://github.com/OAI/OpenAPI-Specification/tree/master/schemas
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..._lazy_import import lazy_import
|
|
6
6
|
|
|
@@ -1919,7 +1919,7 @@ _imports = {
|
|
|
1919
1919
|
}
|
|
1920
1920
|
|
|
1921
1921
|
|
|
1922
|
-
def make_validator(schema:
|
|
1922
|
+
def make_validator(schema: dict[str, Any]) -> Validator:
|
|
1923
1923
|
import jsonschema
|
|
1924
1924
|
|
|
1925
1925
|
return jsonschema.validators.validator_for(schema)(schema)
|