schemathesis 3.21.2__py3-none-any.whl → 3.22.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
|
|
5
5
|
from ._lazy_import import lazy_import
|
|
6
|
-
from .generation import DataGenerationMethod # noqa: E402
|
|
6
|
+
from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
|
|
7
7
|
from .constants import SCHEMATHESIS_VERSION # noqa: E402
|
|
8
8
|
from .models import Case # noqa: E402
|
|
9
9
|
from .specs import openapi # noqa: E402
|
schemathesis/_compat.py
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
from typing import Any, Type, Callable
|
|
2
2
|
from ._lazy_import import lazy_import
|
|
3
3
|
|
|
4
|
-
try:
|
|
5
|
-
from importlib import metadata
|
|
6
|
-
except ImportError:
|
|
7
|
-
import importlib_metadata as metadata # type: ignore
|
|
8
|
-
|
|
9
4
|
|
|
10
5
|
__all__ = [ # noqa: F822
|
|
11
6
|
"JSONMixin",
|
|
@@ -13,7 +8,6 @@ __all__ = [ # noqa: F822
|
|
|
13
8
|
"MultipleFailures",
|
|
14
9
|
"get_signature",
|
|
15
10
|
"get_interesting_origin",
|
|
16
|
-
"metadata",
|
|
17
11
|
"_install_hypothesis_jsonschema_compatibility_shim",
|
|
18
12
|
]
|
|
19
13
|
|
|
@@ -52,26 +46,16 @@ def _load_get_interesting_origin() -> Callable:
|
|
|
52
46
|
|
|
53
47
|
|
|
54
48
|
def _load_multiple_failures() -> Type:
|
|
55
|
-
from ._dependency_versions import IS_HYPOTHESIS_ABOVE_6_54
|
|
56
|
-
|
|
57
49
|
try:
|
|
58
50
|
return BaseExceptionGroup # type: ignore
|
|
59
51
|
except NameError:
|
|
60
|
-
|
|
61
|
-
from exceptiongroup import BaseExceptionGroup as MultipleFailures # type: ignore
|
|
62
|
-
else:
|
|
63
|
-
from hypothesis.errors import MultipleFailures # type: ignore
|
|
52
|
+
from exceptiongroup import BaseExceptionGroup as MultipleFailures # type: ignore
|
|
64
53
|
|
|
65
54
|
return MultipleFailures
|
|
66
55
|
|
|
67
56
|
|
|
68
57
|
def _load_get_signature() -> Callable:
|
|
69
|
-
from .
|
|
70
|
-
|
|
71
|
-
if IS_HYPOTHESIS_ABOVE_6_49:
|
|
72
|
-
from hypothesis.internal.reflection import get_signature
|
|
73
|
-
else:
|
|
74
|
-
from inspect import getfullargspec as get_signature
|
|
58
|
+
from hypothesis.internal.reflection import get_signature
|
|
75
59
|
|
|
76
60
|
return get_signature
|
|
77
61
|
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
"""Compatibility flags based on installed dependency versions."""
|
|
2
2
|
from packaging import version
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from importlib import metadata
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
WERKZEUG_VERSION = version.parse(metadata.version("werkzeug"))
|
|
8
8
|
IS_WERKZEUG_ABOVE_3 = WERKZEUG_VERSION >= version.parse("3.0")
|
|
9
9
|
IS_WERKZEUG_BELOW_2_1 = WERKZEUG_VERSION < version.parse("2.1.0")
|
|
10
10
|
|
|
11
|
-
HYPOTHESIS_VERSION = version.parse(metadata.version("hypothesis"))
|
|
12
|
-
IS_HYPOTHESIS_ABOVE_6_49 = HYPOTHESIS_VERSION >= version.parse("6.49.0")
|
|
13
|
-
IS_HYPOTHESIS_ABOVE_6_54 = HYPOTHESIS_VERSION >= version.parse("6.54.0")
|
|
14
|
-
IS_HYPOTHESIS_ABOVE_6_68_1 = HYPOTHESIS_VERSION >= version.parse("6.68.1")
|
|
15
|
-
|
|
16
11
|
PYTEST_VERSION = version.parse(metadata.version("pytest"))
|
|
17
12
|
IS_PYTEST_ABOVE_54 = PYTEST_VERSION >= version.parse("5.4.0")
|
|
18
13
|
IS_PYTEST_ABOVE_7 = PYTEST_VERSION >= version.parse("7.0.0")
|
schemathesis/_hypothesis.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""High-level API for creating Hypothesis tests."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import asyncio
|
|
3
4
|
import warnings
|
|
4
|
-
from typing import Any, Callable
|
|
5
|
+
from typing import Any, Callable
|
|
5
6
|
|
|
6
7
|
import hypothesis
|
|
7
8
|
from hypothesis import Phase
|
|
@@ -11,7 +12,7 @@ from hypothesis.internal.reflection import proxies
|
|
|
11
12
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
12
13
|
|
|
13
14
|
from .auths import get_auth_storage_from_test
|
|
14
|
-
from .generation import DataGenerationMethod
|
|
15
|
+
from .generation import DataGenerationMethod, GenerationConfig
|
|
15
16
|
from .constants import DEFAULT_DEADLINE
|
|
16
17
|
from .exceptions import OperationSchemaError
|
|
17
18
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
|
@@ -23,12 +24,13 @@ def create_test(
|
|
|
23
24
|
*,
|
|
24
25
|
operation: APIOperation,
|
|
25
26
|
test: Callable,
|
|
26
|
-
settings:
|
|
27
|
-
seed:
|
|
28
|
-
data_generation_methods:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
settings: hypothesis.settings | None = None,
|
|
28
|
+
seed: int | None = None,
|
|
29
|
+
data_generation_methods: list[DataGenerationMethod],
|
|
30
|
+
generation_config: GenerationConfig | None = None,
|
|
31
|
+
as_strategy_kwargs: dict[str, Any] | None = None,
|
|
32
|
+
_given_args: tuple[GivenInput, ...] = (),
|
|
33
|
+
_given_kwargs: dict[str, GivenInput] | None = None,
|
|
32
34
|
) -> Callable:
|
|
33
35
|
"""Create a Hypothesis test."""
|
|
34
36
|
hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
|
|
@@ -40,6 +42,7 @@ def create_test(
|
|
|
40
42
|
hooks=hook_dispatcher,
|
|
41
43
|
auth_storage=auth_storage,
|
|
42
44
|
data_generation_method=data_generation_method,
|
|
45
|
+
generation_config=generation_config,
|
|
43
46
|
**(as_strategy_kwargs or {}),
|
|
44
47
|
)
|
|
45
48
|
)
|
|
@@ -92,7 +95,7 @@ def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
|
|
|
92
95
|
return settings
|
|
93
96
|
|
|
94
97
|
|
|
95
|
-
def _get_hypothesis_settings(test: Callable) ->
|
|
98
|
+
def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
|
|
96
99
|
return getattr(test, "_hypothesis_internal_use_settings", None)
|
|
97
100
|
|
|
98
101
|
|
|
@@ -106,10 +109,10 @@ def make_async_test(test: Callable) -> Callable:
|
|
|
106
109
|
return async_run
|
|
107
110
|
|
|
108
111
|
|
|
109
|
-
def add_examples(test: Callable, operation: APIOperation, hook_dispatcher:
|
|
112
|
+
def add_examples(test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None) -> Callable:
|
|
110
113
|
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
|
111
114
|
try:
|
|
112
|
-
examples:
|
|
115
|
+
examples: list[Case] = [get_single_example(strategy) for strategy in operation.get_strategies_from_examples()]
|
|
113
116
|
except (OperationSchemaError, HypothesisRefResolutionError, Unsatisfiable):
|
|
114
117
|
# Invalid schema:
|
|
115
118
|
# In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
|
|
@@ -143,6 +146,6 @@ def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
|
|
|
143
146
|
def example_generating_inner_function(ex: Case) -> None:
|
|
144
147
|
examples.append(ex)
|
|
145
148
|
|
|
146
|
-
examples:
|
|
149
|
+
examples: list[Case] = []
|
|
147
150
|
example_generating_inner_function()
|
|
148
151
|
return examples[0]
|
schemathesis/_lazy_import.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
def lazy_import(module: str, name: str, imports:
|
|
5
|
+
def lazy_import(module: str, name: str, imports: dict[str, Callable[[], Any]], _globals: dict[str, Any]) -> Any:
|
|
5
6
|
value = _globals.get(name)
|
|
6
7
|
if value is not None:
|
|
7
8
|
return value
|
schemathesis/_xml.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""XML serialization."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
from io import StringIO
|
|
3
4
|
from typing import Any, Dict, List, Union
|
|
4
5
|
from xml.etree import ElementTree
|
|
@@ -12,7 +13,7 @@ DEFAULT_TAG_NAME = "data"
|
|
|
12
13
|
NAMESPACE_URL = "http://example.com/schema"
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def _to_xml(value: Any, raw_schema:
|
|
16
|
+
def _to_xml(value: Any, raw_schema: dict[str, Any], resolved_schema: dict[str, Any]) -> dict[str, Any]:
|
|
16
17
|
"""Serialize a generated Python object as an XML string.
|
|
17
18
|
|
|
18
19
|
Schemas may contain additional information for fine-tuned XML serialization.
|
|
@@ -26,7 +27,7 @@ def _to_xml(value: Any, raw_schema: Dict[str, Any], resolved_schema: Dict[str, A
|
|
|
26
27
|
tag = _get_xml_tag(raw_schema, resolved_schema)
|
|
27
28
|
buffer = StringIO()
|
|
28
29
|
# Collect all namespaces to ensure that all child nodes with prefixes have proper namespaces in their parent nodes
|
|
29
|
-
namespace_stack:
|
|
30
|
+
namespace_stack: list[str] = []
|
|
30
31
|
_write_xml(buffer, value, tag, resolved_schema, namespace_stack)
|
|
31
32
|
data = buffer.getvalue()
|
|
32
33
|
if not is_valid_xml(data):
|
|
@@ -47,7 +48,7 @@ def is_valid_xml(data: str) -> bool:
|
|
|
47
48
|
return False
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def _get_xml_tag(raw_schema:
|
|
51
|
+
def _get_xml_tag(raw_schema: dict[str, Any], resolved_schema: dict[str, Any]) -> str:
|
|
51
52
|
# On the top level we need to detect the proper XML tag, in other cases it is known from object properties
|
|
52
53
|
if resolved_schema.get("xml", {}).get("name"):
|
|
53
54
|
return resolved_schema["xml"]["name"]
|
|
@@ -60,7 +61,7 @@ def _get_xml_tag(raw_schema: Dict[str, Any], resolved_schema: Dict[str, Any]) ->
|
|
|
60
61
|
return DEFAULT_TAG_NAME
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def _write_xml(buffer: StringIO, value: JSON, tag: str, schema:
|
|
64
|
+
def _write_xml(buffer: StringIO, value: JSON, tag: str, schema: dict[str, Any], namespace_stack: list[str]) -> None:
|
|
64
65
|
if isinstance(value, dict):
|
|
65
66
|
_write_object(buffer, value, tag, schema, namespace_stack)
|
|
66
67
|
elif isinstance(value, list):
|
|
@@ -69,7 +70,7 @@ def _write_xml(buffer: StringIO, value: JSON, tag: str, schema: Dict[str, Any],
|
|
|
69
70
|
_write_primitive(buffer, value, tag, schema, namespace_stack)
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
def _validate_prefix(options:
|
|
73
|
+
def _validate_prefix(options: dict[str, Any], namespace_stack: list[str]) -> None:
|
|
73
74
|
try:
|
|
74
75
|
prefix = options["prefix"]
|
|
75
76
|
if prefix not in namespace_stack:
|
|
@@ -78,17 +79,17 @@ def _validate_prefix(options: Dict[str, Any], namespace_stack: List[str]) -> Non
|
|
|
78
79
|
pass
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
def push_namespace_if_any(namespace_stack:
|
|
82
|
+
def push_namespace_if_any(namespace_stack: list[str], options: dict[str, Any]) -> None:
|
|
82
83
|
if "namespace" in options and "prefix" in options:
|
|
83
84
|
namespace_stack.append(options["prefix"])
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
def pop_namespace_if_any(namespace_stack:
|
|
87
|
+
def pop_namespace_if_any(namespace_stack: list[str], options: dict[str, Any]) -> None:
|
|
87
88
|
if "namespace" in options and "prefix" in options:
|
|
88
89
|
namespace_stack.pop()
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
def _write_object(buffer: StringIO, obj:
|
|
92
|
+
def _write_object(buffer: StringIO, obj: dict[str, JSON], tag: str, schema: dict[str, Any], stack: list[str]) -> None:
|
|
92
93
|
options = schema.get("xml", {})
|
|
93
94
|
push_namespace_if_any(stack, options)
|
|
94
95
|
if "prefix" in options:
|
|
@@ -122,7 +123,7 @@ def _write_object(buffer: StringIO, obj: Dict[str, JSON], tag: str, schema: Dict
|
|
|
122
123
|
pop_namespace_if_any(stack, options)
|
|
123
124
|
|
|
124
125
|
|
|
125
|
-
def _write_array(buffer: StringIO, obj:
|
|
126
|
+
def _write_array(buffer: StringIO, obj: list[JSON], tag: str, schema: dict[str, Any], stack: list[str]) -> None:
|
|
126
127
|
options = schema.get("xml", {})
|
|
127
128
|
push_namespace_if_any(stack, options)
|
|
128
129
|
if options.get("prefix"):
|
|
@@ -153,7 +154,7 @@ def _write_array(buffer: StringIO, obj: List[JSON], tag: str, schema: Dict[str,
|
|
|
153
154
|
|
|
154
155
|
|
|
155
156
|
def _write_primitive(
|
|
156
|
-
buffer: StringIO, obj: Primitive, tag: str, schema:
|
|
157
|
+
buffer: StringIO, obj: Primitive, tag: str, schema: dict[str, Any], namespace_stack: list[str]
|
|
157
158
|
) -> None:
|
|
158
159
|
xml_options = schema.get("xml", {})
|
|
159
160
|
# There is no need for modifying the namespace stack, as we know that this function is terminal - it do not recurse
|
|
@@ -165,7 +166,7 @@ def _write_primitive(
|
|
|
165
166
|
buffer.write(f">{obj}</{tag}>")
|
|
166
167
|
|
|
167
168
|
|
|
168
|
-
def _write_namespace(buffer: StringIO, options:
|
|
169
|
+
def _write_namespace(buffer: StringIO, options: dict[str, Any]) -> None:
|
|
169
170
|
buffer.write(" xmlns")
|
|
170
171
|
if "prefix" in options:
|
|
171
172
|
buffer.write(f":{options['prefix']}")
|
schemathesis/auths.py
CHANGED
|
@@ -5,9 +5,16 @@ import threading
|
|
|
5
5
|
import time
|
|
6
6
|
import warnings
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Generic,
|
|
13
|
+
TypeVar,
|
|
14
|
+
overload,
|
|
15
|
+
runtime_checkable,
|
|
16
|
+
Protocol,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
from .exceptions import UsageError
|
|
13
20
|
from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
|
@@ -30,15 +37,15 @@ class AuthContext:
|
|
|
30
37
|
:ivar app: Optional Python application if the WSGI / ASGI integration is used.
|
|
31
38
|
"""
|
|
32
39
|
|
|
33
|
-
operation:
|
|
34
|
-
app:
|
|
40
|
+
operation: APIOperation
|
|
41
|
+
app: Any | None
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
@runtime_checkable
|
|
38
45
|
class AuthProvider(Generic[Auth], Protocol):
|
|
39
46
|
"""Get authentication data for an API and set it on the generated test cases."""
|
|
40
47
|
|
|
41
|
-
def get(self, case:
|
|
48
|
+
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
42
49
|
"""Get the authentication data.
|
|
43
50
|
|
|
44
51
|
:param Case case: Generated test case.
|
|
@@ -46,7 +53,7 @@ class AuthProvider(Generic[Auth], Protocol):
|
|
|
46
53
|
:return: Any authentication data you find useful for your use case. For example, it could be an access token.
|
|
47
54
|
"""
|
|
48
55
|
|
|
49
|
-
def set(self, case:
|
|
56
|
+
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
|
50
57
|
"""Set authentication data on a generated test case.
|
|
51
58
|
|
|
52
59
|
:param Optional[Auth] data: Authentication data you got from the ``get`` method.
|
|
@@ -69,10 +76,10 @@ class RequestsAuth(Generic[Auth]):
|
|
|
69
76
|
|
|
70
77
|
auth: requests.auth.AuthBase
|
|
71
78
|
|
|
72
|
-
def get(self, _:
|
|
79
|
+
def get(self, _: Case, __: AuthContext) -> Auth | None:
|
|
73
80
|
return self.auth # type: ignore[return-value]
|
|
74
81
|
|
|
75
|
-
def set(self, case:
|
|
82
|
+
def set(self, case: Case, _: Auth, __: AuthContext) -> None:
|
|
76
83
|
case._auth = self.auth
|
|
77
84
|
|
|
78
85
|
|
|
@@ -82,12 +89,12 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
82
89
|
|
|
83
90
|
provider: AuthProvider
|
|
84
91
|
refresh_interval: int = DEFAULT_REFRESH_INTERVAL
|
|
85
|
-
cache_entry:
|
|
92
|
+
cache_entry: CacheEntry[Auth] | None = None
|
|
86
93
|
# The timer exists here to simplify testing
|
|
87
94
|
timer: Callable[[], float] = time.monotonic
|
|
88
95
|
_refresh_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
89
96
|
|
|
90
|
-
def get(self, case:
|
|
97
|
+
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
91
98
|
"""Get cached auth value."""
|
|
92
99
|
if self.cache_entry is None or self.timer() >= self.cache_entry.expires:
|
|
93
100
|
with self._refresh_lock:
|
|
@@ -100,7 +107,7 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
100
107
|
return data
|
|
101
108
|
return self.cache_entry.data
|
|
102
109
|
|
|
103
|
-
def set(self, case:
|
|
110
|
+
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
|
104
111
|
"""Set auth data on the `Case` instance.
|
|
105
112
|
|
|
106
113
|
This implementation delegates this to the actual provider.
|
|
@@ -111,33 +118,33 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
111
118
|
class FilterableRegisterAuth(Protocol):
|
|
112
119
|
"""Protocol that adds filters to the return value of `register`."""
|
|
113
120
|
|
|
114
|
-
def __call__(self, provider_class:
|
|
121
|
+
def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
|
115
122
|
pass
|
|
116
123
|
|
|
117
124
|
def apply_to(
|
|
118
125
|
self,
|
|
119
|
-
func:
|
|
126
|
+
func: MatcherFunc | None = None,
|
|
120
127
|
*,
|
|
121
|
-
name:
|
|
122
|
-
name_regex:
|
|
123
|
-
method:
|
|
124
|
-
method_regex:
|
|
125
|
-
path:
|
|
126
|
-
path_regex:
|
|
127
|
-
) ->
|
|
128
|
+
name: FilterValue | None = None,
|
|
129
|
+
name_regex: str | None = None,
|
|
130
|
+
method: FilterValue | None = None,
|
|
131
|
+
method_regex: str | None = None,
|
|
132
|
+
path: FilterValue | None = None,
|
|
133
|
+
path_regex: str | None = None,
|
|
134
|
+
) -> FilterableRegisterAuth:
|
|
128
135
|
pass
|
|
129
136
|
|
|
130
137
|
def skip_for(
|
|
131
138
|
self,
|
|
132
|
-
func:
|
|
139
|
+
func: MatcherFunc | None = None,
|
|
133
140
|
*,
|
|
134
|
-
name:
|
|
135
|
-
name_regex:
|
|
136
|
-
method:
|
|
137
|
-
method_regex:
|
|
138
|
-
path:
|
|
139
|
-
path_regex:
|
|
140
|
-
) ->
|
|
141
|
+
name: FilterValue | None = None,
|
|
142
|
+
name_regex: str | None = None,
|
|
143
|
+
method: FilterValue | None = None,
|
|
144
|
+
method_regex: str | None = None,
|
|
145
|
+
path: FilterValue | None = None,
|
|
146
|
+
path_regex: str | None = None,
|
|
147
|
+
) -> FilterableRegisterAuth:
|
|
141
148
|
pass
|
|
142
149
|
|
|
143
150
|
|
|
@@ -149,28 +156,28 @@ class FilterableApplyAuth(Protocol):
|
|
|
149
156
|
|
|
150
157
|
def apply_to(
|
|
151
158
|
self,
|
|
152
|
-
func:
|
|
159
|
+
func: MatcherFunc | None = None,
|
|
153
160
|
*,
|
|
154
|
-
name:
|
|
155
|
-
name_regex:
|
|
156
|
-
method:
|
|
157
|
-
method_regex:
|
|
158
|
-
path:
|
|
159
|
-
path_regex:
|
|
160
|
-
) ->
|
|
161
|
+
name: FilterValue | None = None,
|
|
162
|
+
name_regex: str | None = None,
|
|
163
|
+
method: FilterValue | None = None,
|
|
164
|
+
method_regex: str | None = None,
|
|
165
|
+
path: FilterValue | None = None,
|
|
166
|
+
path_regex: str | None = None,
|
|
167
|
+
) -> FilterableApplyAuth:
|
|
161
168
|
pass
|
|
162
169
|
|
|
163
170
|
def skip_for(
|
|
164
171
|
self,
|
|
165
|
-
func:
|
|
172
|
+
func: MatcherFunc | None = None,
|
|
166
173
|
*,
|
|
167
|
-
name:
|
|
168
|
-
name_regex:
|
|
169
|
-
method:
|
|
170
|
-
method_regex:
|
|
171
|
-
path:
|
|
172
|
-
path_regex:
|
|
173
|
-
) ->
|
|
174
|
+
name: FilterValue | None = None,
|
|
175
|
+
name_regex: str | None = None,
|
|
176
|
+
method: FilterValue | None = None,
|
|
177
|
+
method_regex: str | None = None,
|
|
178
|
+
path: FilterValue | None = None,
|
|
179
|
+
path_regex: str | None = None,
|
|
180
|
+
) -> FilterableApplyAuth:
|
|
174
181
|
pass
|
|
175
182
|
|
|
176
183
|
|
|
@@ -179,28 +186,28 @@ class FilterableRequestsAuth(Protocol):
|
|
|
179
186
|
|
|
180
187
|
def apply_to(
|
|
181
188
|
self,
|
|
182
|
-
func:
|
|
189
|
+
func: MatcherFunc | None = None,
|
|
183
190
|
*,
|
|
184
|
-
name:
|
|
185
|
-
name_regex:
|
|
186
|
-
method:
|
|
187
|
-
method_regex:
|
|
188
|
-
path:
|
|
189
|
-
path_regex:
|
|
190
|
-
) ->
|
|
191
|
+
name: FilterValue | None = None,
|
|
192
|
+
name_regex: str | None = None,
|
|
193
|
+
method: FilterValue | None = None,
|
|
194
|
+
method_regex: str | None = None,
|
|
195
|
+
path: FilterValue | None = None,
|
|
196
|
+
path_regex: str | None = None,
|
|
197
|
+
) -> FilterableRequestsAuth:
|
|
191
198
|
pass
|
|
192
199
|
|
|
193
200
|
def skip_for(
|
|
194
201
|
self,
|
|
195
|
-
func:
|
|
202
|
+
func: MatcherFunc | None = None,
|
|
196
203
|
*,
|
|
197
|
-
name:
|
|
198
|
-
name_regex:
|
|
199
|
-
method:
|
|
200
|
-
method_regex:
|
|
201
|
-
path:
|
|
202
|
-
path_regex:
|
|
203
|
-
) ->
|
|
204
|
+
name: FilterValue | None = None,
|
|
205
|
+
name_regex: str | None = None,
|
|
206
|
+
method: FilterValue | None = None,
|
|
207
|
+
method_regex: str | None = None,
|
|
208
|
+
path: FilterValue | None = None,
|
|
209
|
+
path_regex: str | None = None,
|
|
210
|
+
) -> FilterableRequestsAuth:
|
|
204
211
|
pass
|
|
205
212
|
|
|
206
213
|
|
|
@@ -211,12 +218,12 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
|
211
218
|
provider: AuthProvider
|
|
212
219
|
filter_set: FilterSet
|
|
213
220
|
|
|
214
|
-
def get(self, case:
|
|
221
|
+
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
215
222
|
if self.filter_set.match(context):
|
|
216
223
|
return _provider_get(self.provider, case, context)
|
|
217
224
|
return None
|
|
218
225
|
|
|
219
|
-
def set(self, case:
|
|
226
|
+
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
|
220
227
|
self.provider.set(case, data, context)
|
|
221
228
|
|
|
222
229
|
|
|
@@ -224,7 +231,7 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
|
224
231
|
class AuthStorage(Generic[Auth]):
|
|
225
232
|
"""Store and manage API authentication."""
|
|
226
233
|
|
|
227
|
-
providers:
|
|
234
|
+
providers: list[AuthProvider] = field(default_factory=list)
|
|
228
235
|
|
|
229
236
|
@property
|
|
230
237
|
def is_defined(self) -> bool:
|
|
@@ -235,25 +242,25 @@ class AuthStorage(Generic[Auth]):
|
|
|
235
242
|
def __call__(
|
|
236
243
|
self,
|
|
237
244
|
*,
|
|
238
|
-
refresh_interval:
|
|
245
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
|
239
246
|
) -> FilterableRegisterAuth:
|
|
240
247
|
pass
|
|
241
248
|
|
|
242
249
|
@overload
|
|
243
250
|
def __call__(
|
|
244
251
|
self,
|
|
245
|
-
provider_class:
|
|
252
|
+
provider_class: type[AuthProvider],
|
|
246
253
|
*,
|
|
247
|
-
refresh_interval:
|
|
254
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
|
248
255
|
) -> FilterableApplyAuth:
|
|
249
256
|
pass
|
|
250
257
|
|
|
251
258
|
def __call__(
|
|
252
259
|
self,
|
|
253
|
-
provider_class:
|
|
260
|
+
provider_class: type[AuthProvider] | None = None,
|
|
254
261
|
*,
|
|
255
|
-
refresh_interval:
|
|
256
|
-
) ->
|
|
262
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
|
263
|
+
) -> FilterableRegisterAuth | FilterableApplyAuth:
|
|
257
264
|
if provider_class is not None:
|
|
258
265
|
return self.apply(provider_class, refresh_interval=refresh_interval)
|
|
259
266
|
return self.register(refresh_interval=refresh_interval)
|
|
@@ -274,8 +281,8 @@ class AuthStorage(Generic[Auth]):
|
|
|
274
281
|
def _set_provider(
|
|
275
282
|
self,
|
|
276
283
|
*,
|
|
277
|
-
provider_class:
|
|
278
|
-
refresh_interval:
|
|
284
|
+
provider_class: type[AuthProvider],
|
|
285
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
|
279
286
|
filter_set: FilterSet,
|
|
280
287
|
) -> None:
|
|
281
288
|
if not issubclass(provider_class, AuthProvider):
|
|
@@ -294,7 +301,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
294
301
|
provider = SelectiveAuthProvider(provider, filter_set)
|
|
295
302
|
self.providers.append(provider)
|
|
296
303
|
|
|
297
|
-
def register(self, *, refresh_interval:
|
|
304
|
+
def register(self, *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL) -> FilterableRegisterAuth:
|
|
298
305
|
"""Register a new auth provider.
|
|
299
306
|
|
|
300
307
|
.. code-block:: python
|
|
@@ -315,7 +322,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
315
322
|
"""
|
|
316
323
|
filter_set = FilterSet()
|
|
317
324
|
|
|
318
|
-
def wrapper(provider_class:
|
|
325
|
+
def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
|
319
326
|
self._set_provider(provider_class=provider_class, refresh_interval=refresh_interval, filter_set=filter_set)
|
|
320
327
|
return provider_class
|
|
321
328
|
|
|
@@ -332,7 +339,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
332
339
|
self.providers = []
|
|
333
340
|
|
|
334
341
|
def apply(
|
|
335
|
-
self, provider_class:
|
|
342
|
+
self, provider_class: type[AuthProvider], *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL
|
|
336
343
|
) -> FilterableApplyAuth:
|
|
337
344
|
"""Register auth provider only on one test function.
|
|
338
345
|
|
|
@@ -366,7 +373,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
366
373
|
return wrapper # type: ignore[return-value]
|
|
367
374
|
|
|
368
375
|
@classmethod
|
|
369
|
-
def add_auth_storage(cls, test: GenericTest) ->
|
|
376
|
+
def add_auth_storage(cls, test: GenericTest) -> AuthStorage:
|
|
370
377
|
"""Attach a new auth storage instance to the test if it is not already present."""
|
|
371
378
|
if not hasattr(test, AUTH_STORAGE_ATTRIBUTE_NAME):
|
|
372
379
|
setattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, cls())
|
|
@@ -374,18 +381,18 @@ class AuthStorage(Generic[Auth]):
|
|
|
374
381
|
raise UsageError(f"`{test.__name__}` has already been decorated with `apply`.")
|
|
375
382
|
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME)
|
|
376
383
|
|
|
377
|
-
def set(self, case:
|
|
384
|
+
def set(self, case: Case, context: AuthContext) -> None:
|
|
378
385
|
"""Set authentication data on a generated test case."""
|
|
379
386
|
if not self.is_defined:
|
|
380
387
|
raise UsageError("No auth provider is defined.")
|
|
381
388
|
for provider in self.providers:
|
|
382
|
-
data:
|
|
389
|
+
data: Auth | None = _provider_get(provider, case, context)
|
|
383
390
|
if data is not None:
|
|
384
391
|
provider.set(case, data, context)
|
|
385
392
|
break
|
|
386
393
|
|
|
387
394
|
|
|
388
|
-
def _provider_get(auth_provider: AuthProvider, case:
|
|
395
|
+
def _provider_get(auth_provider: AuthProvider, case: Case, context: AuthContext) -> Auth | None:
|
|
389
396
|
# A shim to provide a compatibility layer between previously used convention for `AuthProvider.get`
|
|
390
397
|
# where it used to accept a single `context` argument
|
|
391
398
|
method = auth_provider.get
|
|
@@ -404,7 +411,7 @@ def _provider_get(auth_provider: AuthProvider, case: "Case", context: AuthContex
|
|
|
404
411
|
return method(case, context)
|
|
405
412
|
|
|
406
413
|
|
|
407
|
-
def set_on_case(case:
|
|
414
|
+
def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
|
|
408
415
|
"""Set authentication data on this case.
|
|
409
416
|
|
|
410
417
|
If there is no auth defined, then this function is no-op.
|
|
@@ -417,7 +424,7 @@ def set_on_case(case: "Case", context: AuthContext, auth_storage: Optional[AuthS
|
|
|
417
424
|
GLOBAL_AUTH_STORAGE.set(case, context)
|
|
418
425
|
|
|
419
426
|
|
|
420
|
-
def get_auth_storage_from_test(test: GenericTest) ->
|
|
427
|
+
def get_auth_storage_from_test(test: GenericTest) -> AuthStorage | None:
|
|
421
428
|
"""Extract the currently attached auth storage from a test function."""
|
|
422
429
|
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, None)
|
|
423
430
|
|
schemathesis/checks.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from . import failures
|
|
5
5
|
from .exceptions import get_server_error
|
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from .models import Case, CheckFunction
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def not_a_server_error(response: GenericResponse, case: Case) ->
|
|
18
|
+
def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
|
|
19
19
|
"""A check to verify that the response is not a server-side error."""
|
|
20
20
|
status_code = response.status_code
|
|
21
21
|
if status_code >= 500:
|
|
@@ -24,14 +24,14 @@ def not_a_server_error(response: GenericResponse, case: Case) -> Optional[bool]:
|
|
|
24
24
|
return None
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
DEFAULT_CHECKS:
|
|
27
|
+
DEFAULT_CHECKS: tuple[CheckFunction, ...] = (not_a_server_error,)
|
|
28
28
|
OPTIONAL_CHECKS = (
|
|
29
29
|
status_code_conformance,
|
|
30
30
|
content_type_conformance,
|
|
31
31
|
response_headers_conformance,
|
|
32
32
|
response_schema_conformance,
|
|
33
33
|
)
|
|
34
|
-
ALL_CHECKS:
|
|
34
|
+
ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def register(check: CheckFunction) -> CheckFunction:
|