schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
@@ -1,82 +1,44 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import Any
|
3
|
-
|
4
|
-
from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
|
5
|
-
from ._lazy_import import lazy_import
|
6
|
-
from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
|
7
|
-
from .constants import SCHEMATHESIS_VERSION # noqa: E402
|
8
|
-
from .models import Case # noqa: E402
|
9
|
-
from .specs import openapi # noqa: E402
|
10
2
|
|
3
|
+
from schemathesis import auths, contrib, engine, errors, experimental, graphql, hooks, openapi, pytest, python
|
4
|
+
from schemathesis.checks import CheckContext, CheckFunction, check
|
5
|
+
from schemathesis.core.output import OutputConfig, sanitization
|
6
|
+
from schemathesis.core.transport import Response
|
7
|
+
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
8
|
+
from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
|
9
|
+
from schemathesis.generation.case import Case
|
10
|
+
from schemathesis.generation.targets import TargetContext, TargetFunction, target
|
11
11
|
|
12
12
|
__version__ = SCHEMATHESIS_VERSION
|
13
13
|
|
14
|
-
# Default loaders
|
15
|
-
from_aiohttp = openapi.from_aiohttp
|
16
|
-
from_asgi = openapi.from_asgi
|
17
|
-
from_dict = openapi.from_dict
|
18
|
-
from_file = openapi.from_file
|
19
|
-
from_path = openapi.from_path
|
20
|
-
from_pytest_fixture = openapi.from_pytest_fixture
|
21
|
-
from_uri = openapi.from_uri
|
22
|
-
from_wsgi = openapi.from_wsgi
|
23
|
-
|
24
14
|
# Public API
|
25
15
|
auth = auths.GLOBAL_AUTH_STORAGE
|
26
|
-
check = checks.register
|
27
16
|
hook = hooks.register
|
28
|
-
serializer = serializers.register
|
29
|
-
target = targets.register
|
30
|
-
|
31
|
-
# Backward compatibility
|
32
|
-
register_check = checks.register
|
33
|
-
register_target = targets.register
|
34
|
-
register_string_format = openapi.format
|
35
17
|
|
36
18
|
__all__ = [
|
37
|
-
"auths",
|
38
|
-
"checks",
|
39
|
-
"experimental",
|
40
|
-
"contrib",
|
41
|
-
"fixups",
|
42
|
-
"graphql",
|
43
|
-
"hooks",
|
44
|
-
"runner",
|
45
|
-
"serializers",
|
46
|
-
"targets",
|
47
|
-
"DataGenerationMethod",
|
48
|
-
"SCHEMATHESIS_VERSION",
|
49
19
|
"Case",
|
50
|
-
"
|
20
|
+
"CheckContext",
|
21
|
+
"CheckFunction",
|
22
|
+
"GenerationMode",
|
23
|
+
"GenerationConfig",
|
24
|
+
"HeaderConfig",
|
25
|
+
"OutputConfig",
|
26
|
+
"Response",
|
27
|
+
"TargetContext",
|
28
|
+
"TargetFunction",
|
51
29
|
"__version__",
|
52
|
-
"from_aiohttp",
|
53
|
-
"from_asgi",
|
54
|
-
"from_dict",
|
55
|
-
"from_file",
|
56
|
-
"from_path",
|
57
|
-
"from_pytest_fixture",
|
58
|
-
"from_uri",
|
59
|
-
"from_wsgi",
|
60
30
|
"auth",
|
61
31
|
"check",
|
32
|
+
"contrib",
|
33
|
+
"engine",
|
34
|
+
"errors",
|
35
|
+
"experimental",
|
36
|
+
"graphql",
|
62
37
|
"hook",
|
63
|
-
"
|
38
|
+
"hooks",
|
39
|
+
"openapi",
|
40
|
+
"pytest",
|
41
|
+
"python",
|
42
|
+
"sanitization",
|
64
43
|
"target",
|
65
|
-
"register_check",
|
66
|
-
"register_target",
|
67
|
-
"register_string_format",
|
68
44
|
]
|
69
|
-
|
70
|
-
|
71
|
-
def _load_generic_response() -> Any:
|
72
|
-
from .transports.responses import GenericResponse
|
73
|
-
|
74
|
-
return GenericResponse
|
75
|
-
|
76
|
-
|
77
|
-
_imports = {"GenericResponse": _load_generic_response}
|
78
|
-
|
79
|
-
|
80
|
-
def __getattr__(name: str) -> Any:
|
81
|
-
# Some modules are relatively heavy, hence load them lazily to improve startup time for CLI
|
82
|
-
return lazy_import(__name__, name, _imports, globals())
|
schemathesis/auths.py
CHANGED
@@ -1,31 +1,34 @@
|
|
1
1
|
"""Support for custom API authentication mechanisms."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
3
|
-
|
4
|
+
|
4
5
|
import threading
|
5
6
|
import time
|
6
|
-
import warnings
|
7
7
|
from dataclasses import dataclass, field
|
8
8
|
from typing import (
|
9
9
|
TYPE_CHECKING,
|
10
10
|
Any,
|
11
11
|
Callable,
|
12
12
|
Generic,
|
13
|
+
Protocol,
|
13
14
|
TypeVar,
|
15
|
+
Union,
|
14
16
|
overload,
|
15
17
|
runtime_checkable,
|
16
|
-
Protocol,
|
17
18
|
)
|
18
19
|
|
19
|
-
from .
|
20
|
-
from .
|
21
|
-
from .
|
20
|
+
from schemathesis.core.errors import IncorrectUsage
|
21
|
+
from schemathesis.core.marks import Mark
|
22
|
+
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
23
|
+
from schemathesis.generation.case import Case
|
22
24
|
|
23
25
|
if TYPE_CHECKING:
|
24
|
-
from .models import APIOperation, Case
|
25
26
|
import requests.auth
|
26
27
|
|
28
|
+
from schemathesis.schemas import APIOperation
|
29
|
+
|
27
30
|
DEFAULT_REFRESH_INTERVAL = 300
|
28
|
-
|
31
|
+
AuthStorageMark = Mark["AuthStorage"](attr_name="auth_storage")
|
29
32
|
Auth = TypeVar("Auth")
|
30
33
|
|
31
34
|
|
@@ -41,6 +44,9 @@ class AuthContext:
|
|
41
44
|
app: Any | None
|
42
45
|
|
43
46
|
|
47
|
+
CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
48
|
+
|
49
|
+
|
44
50
|
@runtime_checkable
|
45
51
|
class AuthProvider(Generic[Auth], Protocol):
|
46
52
|
"""Get authentication data for an API and set it on the generated test cases."""
|
@@ -96,16 +102,24 @@ class CachingAuthProvider(Generic[Auth]):
|
|
96
102
|
|
97
103
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
98
104
|
"""Get cached auth value."""
|
99
|
-
|
105
|
+
cache_entry = self._get_cache_entry(case, context)
|
106
|
+
if cache_entry is None or self.timer() >= cache_entry.expires:
|
100
107
|
with self._refresh_lock:
|
101
|
-
|
108
|
+
cache_entry = self._get_cache_entry(case, context)
|
109
|
+
if not (cache_entry is None or self.timer() >= cache_entry.expires):
|
102
110
|
# Another thread updated the cache
|
103
|
-
return
|
111
|
+
return cache_entry.data
|
104
112
|
# We know that optional auth is possible only inside a higher-level wrapper
|
105
|
-
data: Auth =
|
106
|
-
self.
|
113
|
+
data: Auth = self.provider.get(case, context) # type: ignore[assignment]
|
114
|
+
self._set_cache_entry(data, case, context)
|
107
115
|
return data
|
108
|
-
return
|
116
|
+
return cache_entry.data
|
117
|
+
|
118
|
+
def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
|
119
|
+
return self.cache_entry
|
120
|
+
|
121
|
+
def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
|
122
|
+
self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
|
109
123
|
|
110
124
|
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
111
125
|
"""Set auth data on the `Case` instance.
|
@@ -115,11 +129,29 @@ class CachingAuthProvider(Generic[Auth]):
|
|
115
129
|
self.provider.set(case, data, context)
|
116
130
|
|
117
131
|
|
132
|
+
def _noop_key_function(case: Case, context: AuthContext) -> str:
|
133
|
+
# Never used
|
134
|
+
raise NotImplementedError
|
135
|
+
|
136
|
+
|
137
|
+
@dataclass
|
138
|
+
class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
|
139
|
+
cache_by_key: CacheKeyFunction = _noop_key_function
|
140
|
+
cache_entries: dict[str | int, CacheEntry[Auth] | None] = field(default_factory=dict)
|
141
|
+
|
142
|
+
def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
|
143
|
+
key = self.cache_by_key(case, context)
|
144
|
+
return self.cache_entries.get(key)
|
145
|
+
|
146
|
+
def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
|
147
|
+
key = self.cache_by_key(case, context)
|
148
|
+
self.cache_entries[key] = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
|
149
|
+
|
150
|
+
|
118
151
|
class FilterableRegisterAuth(Protocol):
|
119
152
|
"""Protocol that adds filters to the return value of `register`."""
|
120
153
|
|
121
|
-
def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
122
|
-
pass
|
154
|
+
def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
|
123
155
|
|
124
156
|
def apply_to(
|
125
157
|
self,
|
@@ -131,8 +163,7 @@ class FilterableRegisterAuth(Protocol):
|
|
131
163
|
method_regex: str | None = None,
|
132
164
|
path: FilterValue | None = None,
|
133
165
|
path_regex: str | None = None,
|
134
|
-
) -> FilterableRegisterAuth:
|
135
|
-
pass
|
166
|
+
) -> FilterableRegisterAuth: ...
|
136
167
|
|
137
168
|
def skip_for(
|
138
169
|
self,
|
@@ -144,15 +175,13 @@ class FilterableRegisterAuth(Protocol):
|
|
144
175
|
method_regex: str | None = None,
|
145
176
|
path: FilterValue | None = None,
|
146
177
|
path_regex: str | None = None,
|
147
|
-
) -> FilterableRegisterAuth:
|
148
|
-
pass
|
178
|
+
) -> FilterableRegisterAuth: ...
|
149
179
|
|
150
180
|
|
151
181
|
class FilterableApplyAuth(Protocol):
|
152
182
|
"""Protocol that adds filters to the return value of `apply`."""
|
153
183
|
|
154
|
-
def __call__(self, test:
|
155
|
-
pass
|
184
|
+
def __call__(self, test: Callable) -> Callable: ...
|
156
185
|
|
157
186
|
def apply_to(
|
158
187
|
self,
|
@@ -164,8 +193,7 @@ class FilterableApplyAuth(Protocol):
|
|
164
193
|
method_regex: str | None = None,
|
165
194
|
path: FilterValue | None = None,
|
166
195
|
path_regex: str | None = None,
|
167
|
-
) -> FilterableApplyAuth:
|
168
|
-
pass
|
196
|
+
) -> FilterableApplyAuth: ...
|
169
197
|
|
170
198
|
def skip_for(
|
171
199
|
self,
|
@@ -177,8 +205,7 @@ class FilterableApplyAuth(Protocol):
|
|
177
205
|
method_regex: str | None = None,
|
178
206
|
path: FilterValue | None = None,
|
179
207
|
path_regex: str | None = None,
|
180
|
-
) -> FilterableApplyAuth:
|
181
|
-
pass
|
208
|
+
) -> FilterableApplyAuth: ...
|
182
209
|
|
183
210
|
|
184
211
|
class FilterableRequestsAuth(Protocol):
|
@@ -194,8 +221,7 @@ class FilterableRequestsAuth(Protocol):
|
|
194
221
|
method_regex: str | None = None,
|
195
222
|
path: FilterValue | None = None,
|
196
223
|
path_regex: str | None = None,
|
197
|
-
) -> FilterableRequestsAuth:
|
198
|
-
pass
|
224
|
+
) -> FilterableRequestsAuth: ...
|
199
225
|
|
200
226
|
def skip_for(
|
201
227
|
self,
|
@@ -207,8 +233,7 @@ class FilterableRequestsAuth(Protocol):
|
|
207
233
|
method_regex: str | None = None,
|
208
234
|
path: FilterValue | None = None,
|
209
235
|
path_regex: str | None = None,
|
210
|
-
) -> FilterableRequestsAuth:
|
211
|
-
pass
|
236
|
+
) -> FilterableRequestsAuth: ...
|
212
237
|
|
213
238
|
|
214
239
|
@dataclass
|
@@ -220,7 +245,7 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
220
245
|
|
221
246
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
222
247
|
if self.filter_set.match(context):
|
223
|
-
return
|
248
|
+
return self.provider.get(case, context)
|
224
249
|
return None
|
225
250
|
|
226
251
|
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
@@ -243,8 +268,8 @@ class AuthStorage(Generic[Auth]):
|
|
243
268
|
self,
|
244
269
|
*,
|
245
270
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
246
|
-
|
247
|
-
|
271
|
+
cache_by_key: CacheKeyFunction | None = None,
|
272
|
+
) -> FilterableRegisterAuth: ...
|
248
273
|
|
249
274
|
@overload
|
250
275
|
def __call__(
|
@@ -252,26 +277,26 @@ class AuthStorage(Generic[Auth]):
|
|
252
277
|
provider_class: type[AuthProvider],
|
253
278
|
*,
|
254
279
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
255
|
-
|
256
|
-
|
280
|
+
cache_by_key: CacheKeyFunction | None = None,
|
281
|
+
) -> FilterableApplyAuth: ...
|
257
282
|
|
258
283
|
def __call__(
|
259
284
|
self,
|
260
285
|
provider_class: type[AuthProvider] | None = None,
|
261
286
|
*,
|
262
287
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
288
|
+
cache_by_key: CacheKeyFunction | None = None,
|
263
289
|
) -> FilterableRegisterAuth | FilterableApplyAuth:
|
264
290
|
if provider_class is not None:
|
265
|
-
return self.apply(provider_class, refresh_interval=refresh_interval)
|
266
|
-
return self.register(refresh_interval=refresh_interval)
|
291
|
+
return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
292
|
+
return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
267
293
|
|
268
294
|
def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
|
269
295
|
"""Use `requests` auth instance as an auth provider."""
|
270
296
|
filter_set = FilterSet()
|
271
297
|
self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
|
272
298
|
|
273
|
-
class _FilterableRequestsAuth:
|
274
|
-
pass
|
299
|
+
class _FilterableRequestsAuth: ...
|
275
300
|
|
276
301
|
attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
|
277
302
|
attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
|
@@ -283,6 +308,7 @@ class AuthStorage(Generic[Auth]):
|
|
283
308
|
*,
|
284
309
|
provider_class: type[AuthProvider],
|
285
310
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
311
|
+
cache_by_key: CacheKeyFunction | None = None,
|
286
312
|
filter_set: FilterSet,
|
287
313
|
) -> None:
|
288
314
|
if not issubclass(provider_class, AuthProvider):
|
@@ -292,16 +318,27 @@ class AuthStorage(Generic[Auth]):
|
|
292
318
|
)
|
293
319
|
provider: AuthProvider
|
294
320
|
# Apply caching if desired
|
321
|
+
instance = provider_class()
|
295
322
|
if refresh_interval is not None:
|
296
|
-
|
323
|
+
if cache_by_key is None:
|
324
|
+
provider = CachingAuthProvider(instance, refresh_interval=refresh_interval)
|
325
|
+
else:
|
326
|
+
provider = KeyedCachingAuthProvider(
|
327
|
+
instance, refresh_interval=refresh_interval, cache_by_key=cache_by_key
|
328
|
+
)
|
297
329
|
else:
|
298
|
-
provider =
|
330
|
+
provider = instance
|
299
331
|
# Store filters if any
|
300
332
|
if not filter_set.is_empty():
|
301
333
|
provider = SelectiveAuthProvider(provider, filter_set)
|
302
334
|
self.providers.append(provider)
|
303
335
|
|
304
|
-
def register(
|
336
|
+
def register(
|
337
|
+
self,
|
338
|
+
*,
|
339
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
340
|
+
cache_by_key: CacheKeyFunction | None = None,
|
341
|
+
) -> FilterableRegisterAuth:
|
305
342
|
"""Register a new auth provider.
|
306
343
|
|
307
344
|
.. code-block:: python
|
@@ -323,7 +360,12 @@ class AuthStorage(Generic[Auth]):
|
|
323
360
|
filter_set = FilterSet()
|
324
361
|
|
325
362
|
def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
326
|
-
self._set_provider(
|
363
|
+
self._set_provider(
|
364
|
+
provider_class=provider_class,
|
365
|
+
refresh_interval=refresh_interval,
|
366
|
+
filter_set=filter_set,
|
367
|
+
cache_by_key=cache_by_key,
|
368
|
+
)
|
327
369
|
return provider_class
|
328
370
|
|
329
371
|
attach_filter_chain(wrapper, "apply_to", filter_set.include)
|
@@ -339,7 +381,11 @@ class AuthStorage(Generic[Auth]):
|
|
339
381
|
self.providers = []
|
340
382
|
|
341
383
|
def apply(
|
342
|
-
self,
|
384
|
+
self,
|
385
|
+
provider_class: type[AuthProvider],
|
386
|
+
*,
|
387
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
388
|
+
cache_by_key: CacheKeyFunction | None = None,
|
343
389
|
) -> FilterableApplyAuth:
|
344
390
|
"""Register auth provider only on one test function.
|
345
391
|
|
@@ -360,10 +406,16 @@ class AuthStorage(Generic[Auth]):
|
|
360
406
|
"""
|
361
407
|
filter_set = FilterSet()
|
362
408
|
|
363
|
-
def wrapper(test:
|
364
|
-
|
409
|
+
def wrapper(test: Callable) -> Callable:
|
410
|
+
if AuthStorageMark.is_set(test):
|
411
|
+
raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `apply`.")
|
412
|
+
auth_storage = self.__class__()
|
413
|
+
AuthStorageMark.set(test, auth_storage)
|
365
414
|
auth_storage._set_provider(
|
366
|
-
provider_class=provider_class,
|
415
|
+
provider_class=provider_class,
|
416
|
+
refresh_interval=refresh_interval,
|
417
|
+
filter_set=filter_set,
|
418
|
+
cache_by_key=cache_by_key,
|
367
419
|
)
|
368
420
|
return test
|
369
421
|
|
@@ -372,45 +424,18 @@ class AuthStorage(Generic[Auth]):
|
|
372
424
|
|
373
425
|
return wrapper # type: ignore[return-value]
|
374
426
|
|
375
|
-
@classmethod
|
376
|
-
def add_auth_storage(cls, test: GenericTest) -> AuthStorage:
|
377
|
-
"""Attach a new auth storage instance to the test if it is not already present."""
|
378
|
-
if not hasattr(test, AUTH_STORAGE_ATTRIBUTE_NAME):
|
379
|
-
setattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, cls())
|
380
|
-
else:
|
381
|
-
raise UsageError(f"`{test.__name__}` has already been decorated with `apply`.")
|
382
|
-
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME)
|
383
|
-
|
384
427
|
def set(self, case: Case, context: AuthContext) -> None:
|
385
428
|
"""Set authentication data on a generated test case."""
|
386
429
|
if not self.is_defined:
|
387
|
-
raise
|
430
|
+
raise IncorrectUsage("No auth provider is defined.")
|
388
431
|
for provider in self.providers:
|
389
|
-
data: Auth | None =
|
432
|
+
data: Auth | None = provider.get(case, context)
|
390
433
|
if data is not None:
|
391
434
|
provider.set(case, data, context)
|
435
|
+
case._has_explicit_auth = True
|
392
436
|
break
|
393
437
|
|
394
438
|
|
395
|
-
def _provider_get(auth_provider: AuthProvider, case: Case, context: AuthContext) -> Auth | None:
|
396
|
-
# A shim to provide a compatibility layer between previously used convention for `AuthProvider.get`
|
397
|
-
# where it used to accept a single `context` argument
|
398
|
-
method = auth_provider.get
|
399
|
-
parameters = inspect.signature(method).parameters
|
400
|
-
if len(parameters) == 1:
|
401
|
-
# Old calling convention
|
402
|
-
warnings.warn(
|
403
|
-
"The method 'get' of your AuthProvider is using the old calling convention, "
|
404
|
-
"which is deprecated and will be removed in Schemathesis 4.0. "
|
405
|
-
"Please update it to accept both 'case' and 'context' as arguments.",
|
406
|
-
DeprecationWarning,
|
407
|
-
stacklevel=1,
|
408
|
-
)
|
409
|
-
return method(context) # type: ignore
|
410
|
-
# New calling convention
|
411
|
-
return method(case, context)
|
412
|
-
|
413
|
-
|
414
439
|
def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
|
415
440
|
"""Set authentication data on this case.
|
416
441
|
|
@@ -424,11 +449,6 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
424
449
|
GLOBAL_AUTH_STORAGE.set(case, context)
|
425
450
|
|
426
451
|
|
427
|
-
def get_auth_storage_from_test(test: GenericTest) -> AuthStorage | None:
|
428
|
-
"""Extract the currently attached auth storage from a test function."""
|
429
|
-
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, None)
|
430
|
-
|
431
|
-
|
432
452
|
# Global auth API
|
433
453
|
GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
|
434
454
|
register = GLOBAL_AUTH_STORAGE.register
|
schemathesis/checks.py
CHANGED
@@ -1,68 +1,148 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
2
|
+
|
3
3
|
import json
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
|
4
5
|
|
5
|
-
from . import
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
from schemathesis.core.failures import (
|
7
|
+
Failure,
|
8
|
+
FailureGroup,
|
9
|
+
MalformedJson,
|
10
|
+
MaxResponseTimeConfig,
|
11
|
+
ResponseTimeExceeded,
|
12
|
+
ServerError,
|
12
13
|
)
|
14
|
+
from schemathesis.core.registries import Registry
|
15
|
+
from schemathesis.core.transport import Response
|
16
|
+
from schemathesis.generation.overrides import Override
|
13
17
|
|
14
18
|
if TYPE_CHECKING:
|
15
|
-
from .
|
16
|
-
|
19
|
+
from requests.models import CaseInsensitiveDict
|
20
|
+
|
21
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
22
|
+
from schemathesis.generation.case import Case
|
23
|
+
|
24
|
+
CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
|
25
|
+
ChecksConfig = dict[CheckFunction, Any]
|
26
|
+
|
27
|
+
|
28
|
+
class CheckContext:
|
29
|
+
"""Context for Schemathesis checks.
|
30
|
+
|
31
|
+
Provides access to broader test execution data beyond individual test cases.
|
32
|
+
"""
|
33
|
+
|
34
|
+
override: Override | None
|
35
|
+
auth: tuple[str, str] | None
|
36
|
+
headers: CaseInsensitiveDict | None
|
37
|
+
config: ChecksConfig
|
38
|
+
transport_kwargs: dict[str, Any] | None
|
39
|
+
recorder: ScenarioRecorder | None
|
40
|
+
|
41
|
+
__slots__ = ("override", "auth", "headers", "config", "transport_kwargs", "recorder")
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
override: Override | None,
|
46
|
+
auth: tuple[str, str] | None,
|
47
|
+
headers: CaseInsensitiveDict | None,
|
48
|
+
config: ChecksConfig,
|
49
|
+
transport_kwargs: dict[str, Any] | None,
|
50
|
+
recorder: ScenarioRecorder | None = None,
|
51
|
+
) -> None:
|
52
|
+
self.override = override
|
53
|
+
self.auth = auth
|
54
|
+
self.headers = headers
|
55
|
+
self.config = config
|
56
|
+
self.transport_kwargs = transport_kwargs
|
57
|
+
self.recorder = recorder
|
58
|
+
|
59
|
+
def find_parent(self, *, case_id: str) -> Case | None:
|
60
|
+
if self.recorder is not None:
|
61
|
+
return self.recorder.find_parent(case_id=case_id)
|
62
|
+
return None
|
63
|
+
|
64
|
+
def find_related(self, *, case_id: str) -> Iterator[Case]:
|
65
|
+
if self.recorder is not None:
|
66
|
+
yield from self.recorder.find_related(case_id=case_id)
|
17
67
|
|
68
|
+
def find_response(self, *, case_id: str) -> Response | None:
|
69
|
+
if self.recorder is not None:
|
70
|
+
return self.recorder.find_response(case_id=case_id)
|
71
|
+
return None
|
18
72
|
|
19
|
-
def
|
73
|
+
def record_case(self, *, parent_id: str, case: Case) -> None:
|
74
|
+
if self.recorder is not None:
|
75
|
+
self.recorder.record_case(parent_id=parent_id, case=case)
|
76
|
+
|
77
|
+
def record_response(self, *, case_id: str, response: Response) -> None:
|
78
|
+
if self.recorder is not None:
|
79
|
+
self.recorder.record_response(case_id=case_id, response=response)
|
80
|
+
|
81
|
+
|
82
|
+
CHECKS = Registry[CheckFunction]()
|
83
|
+
check = CHECKS.register
|
84
|
+
|
85
|
+
|
86
|
+
@check
|
87
|
+
def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
20
88
|
"""A check to verify that the response is not a server-side error."""
|
21
|
-
from .specs.graphql.schemas import
|
89
|
+
from .specs.graphql.schemas import GraphQLSchema
|
22
90
|
from .specs.graphql.validation import validate_graphql_response
|
23
|
-
from .transports.responses import get_json
|
24
91
|
|
25
92
|
status_code = response.status_code
|
26
93
|
if status_code >= 500:
|
27
|
-
|
28
|
-
|
29
|
-
if isinstance(case, GraphQLCase):
|
94
|
+
raise ServerError(operation=case.operation.label, status_code=status_code)
|
95
|
+
if isinstance(case.operation.schema, GraphQLSchema):
|
30
96
|
try:
|
31
|
-
data =
|
32
|
-
validate_graphql_response(data)
|
97
|
+
data = response.json()
|
98
|
+
validate_graphql_response(case, data)
|
33
99
|
except json.JSONDecodeError as exc:
|
34
|
-
|
35
|
-
context = failures.JSONDecodeErrorContext.from_exception(exc)
|
36
|
-
raise exc_class(context.title, context=context) from exc
|
100
|
+
raise MalformedJson.from_exception(operation=case.operation.label, exc=exc) from None
|
37
101
|
return None
|
38
102
|
|
39
103
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
"""Register a new check for schemathesis CLI.
|
52
|
-
|
53
|
-
:param check: A function to validate API responses.
|
104
|
+
def max_response_time(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
105
|
+
config = ctx.config.get(max_response_time, MaxResponseTimeConfig())
|
106
|
+
elapsed = response.elapsed
|
107
|
+
if elapsed > config.limit:
|
108
|
+
raise ResponseTimeExceeded(
|
109
|
+
operation=case.operation.label,
|
110
|
+
message=f"Actual: {elapsed:.2f}ms\nLimit: {config.limit * 1000:.2f}ms",
|
111
|
+
elapsed=elapsed,
|
112
|
+
deadline=config.limit,
|
113
|
+
)
|
114
|
+
return None
|
54
115
|
|
55
|
-
.. code-block:: python
|
56
116
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
117
|
+
def run_checks(
|
118
|
+
*,
|
119
|
+
case: Case,
|
120
|
+
response: Response,
|
121
|
+
ctx: CheckContext,
|
122
|
+
checks: Iterable[CheckFunction],
|
123
|
+
on_failure: Callable[[str, set[Failure], Failure], None],
|
124
|
+
on_success: Callable[[str, Case], None] | None = None,
|
125
|
+
) -> set[Failure]:
|
126
|
+
"""Run a set of checks against a response."""
|
127
|
+
collected: set[Failure] = set()
|
63
128
|
|
64
|
-
|
129
|
+
for check in checks:
|
130
|
+
name = check.__name__
|
131
|
+
try:
|
132
|
+
skip_check = check(ctx, response, case)
|
133
|
+
if not skip_check and on_success:
|
134
|
+
on_success(name, case)
|
135
|
+
except Failure as failure:
|
136
|
+
on_failure(name, collected, failure.with_traceback(None))
|
137
|
+
except AssertionError as exc:
|
138
|
+
custom_failure = Failure.from_assertion(
|
139
|
+
name=name,
|
140
|
+
operation=case.operation.label,
|
141
|
+
exc=exc,
|
142
|
+
)
|
143
|
+
on_failure(name, collected, custom_failure)
|
144
|
+
except FailureGroup as group:
|
145
|
+
for sub_failure in group.exceptions:
|
146
|
+
on_failure(name, collected, sub_failure)
|
65
147
|
|
66
|
-
|
67
|
-
cli.CHECKS_TYPE.choices += (check.__name__,) # type: ignore
|
68
|
-
return check
|
148
|
+
return collected
|