schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -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 +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- 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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/__init__.py
CHANGED
@@ -1,90 +1,52 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
|
5
|
-
from . import
|
6
|
-
from .
|
7
|
-
from .
|
8
|
-
from .
|
9
|
-
from .
|
10
|
-
from .
|
11
|
-
from .
|
3
|
+
from schemathesis import errors, graphql, openapi, pytest
|
4
|
+
from schemathesis.auths import AuthContext, AuthProvider, auth
|
5
|
+
from schemathesis.checks import CheckContext, CheckFunction, check
|
6
|
+
from schemathesis.config import SchemathesisConfig as Config
|
7
|
+
from schemathesis.core.transport import Response
|
8
|
+
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
9
|
+
from schemathesis.generation import GenerationMode, stateful
|
10
|
+
from schemathesis.generation.case import Case
|
11
|
+
from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
|
12
|
+
from schemathesis.hooks import HookContext, hook
|
13
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
14
|
+
from schemathesis.transport import SerializationContext, serializer
|
12
15
|
|
13
16
|
__version__ = SCHEMATHESIS_VERSION
|
14
17
|
|
15
|
-
# Default loaders
|
16
|
-
from_aiohttp = openapi.from_aiohttp
|
17
|
-
from_asgi = openapi.from_asgi
|
18
|
-
from_dict = openapi.from_dict
|
19
|
-
from_file = openapi.from_file
|
20
|
-
from_path = openapi.from_path
|
21
|
-
from_pytest_fixture = openapi.from_pytest_fixture
|
22
|
-
from_uri = openapi.from_uri
|
23
|
-
from_wsgi = openapi.from_wsgi
|
24
|
-
|
25
|
-
# Public API
|
26
|
-
auth = auths.GLOBAL_AUTH_STORAGE
|
27
|
-
check = checks.register
|
28
|
-
hook = hooks.register
|
29
|
-
serializer = serializers.register
|
30
|
-
target = targets.register
|
31
|
-
|
32
|
-
# Backward compatibility
|
33
|
-
register_check = checks.register
|
34
|
-
register_target = targets.register
|
35
|
-
register_string_format = openapi.format
|
36
|
-
|
37
18
|
__all__ = [
|
38
|
-
"
|
39
|
-
|
40
|
-
"experimental",
|
41
|
-
"contrib",
|
42
|
-
"fixups",
|
43
|
-
"graphql",
|
44
|
-
"hooks",
|
45
|
-
"runner",
|
46
|
-
"serializers",
|
47
|
-
"targets",
|
48
|
-
"DataGenerationMethod",
|
49
|
-
"SCHEMATHESIS_VERSION",
|
19
|
+
"__version__",
|
20
|
+
# Core data structures
|
50
21
|
"Case",
|
22
|
+
"Response",
|
23
|
+
"APIOperation",
|
24
|
+
"BaseSchema",
|
25
|
+
"Config",
|
26
|
+
"GenerationMode",
|
27
|
+
"stateful",
|
28
|
+
# Public errors
|
29
|
+
"errors",
|
30
|
+
# Spec or usage specific namespaces
|
51
31
|
"openapi",
|
52
|
-
"
|
53
|
-
"
|
54
|
-
|
55
|
-
"from_dict",
|
56
|
-
"from_file",
|
57
|
-
"from_path",
|
58
|
-
"from_pytest_fixture",
|
59
|
-
"from_uri",
|
60
|
-
"from_wsgi",
|
61
|
-
"auth",
|
62
|
-
"check",
|
32
|
+
"graphql",
|
33
|
+
"pytest",
|
34
|
+
# Hooks
|
63
35
|
"hook",
|
64
|
-
"serializer",
|
65
|
-
"target",
|
66
|
-
"register_check",
|
67
|
-
"register_target",
|
68
|
-
"register_string_format",
|
69
36
|
"HookContext",
|
37
|
+
# Checks
|
38
|
+
"check",
|
39
|
+
"CheckContext",
|
40
|
+
"CheckFunction",
|
41
|
+
# Auth
|
42
|
+
"auth",
|
43
|
+
"AuthContext",
|
44
|
+
"AuthProvider",
|
45
|
+
# Targeted Property-based Testing
|
46
|
+
"metric",
|
47
|
+
"MetricContext",
|
48
|
+
"MetricFunction",
|
49
|
+
# Serialization
|
50
|
+
"serializer",
|
51
|
+
"SerializationContext",
|
70
52
|
]
|
71
|
-
|
72
|
-
|
73
|
-
def _load_generic_response() -> Any:
|
74
|
-
from .transports.responses import GenericResponse
|
75
|
-
|
76
|
-
return GenericResponse
|
77
|
-
|
78
|
-
|
79
|
-
def _load_base_schema() -> Any:
|
80
|
-
from .schemas import BaseSchema
|
81
|
-
|
82
|
-
return BaseSchema
|
83
|
-
|
84
|
-
|
85
|
-
_imports = {"GenericResponse": _load_generic_response, "BaseSchema": _load_base_schema}
|
86
|
-
|
87
|
-
|
88
|
-
def __getattr__(name: str) -> Any:
|
89
|
-
# Some modules are relatively heavy, hence load them lazily to improve startup time for CLI
|
90
|
-
return lazy_import(__name__, name, _imports, globals())
|
schemathesis/auths.py
CHANGED
@@ -2,10 +2,8 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
import inspect
|
6
5
|
import threading
|
7
6
|
import time
|
8
|
-
import warnings
|
9
7
|
from dataclasses import dataclass, field
|
10
8
|
from typing import (
|
11
9
|
TYPE_CHECKING,
|
@@ -19,30 +17,51 @@ from typing import (
|
|
19
17
|
runtime_checkable,
|
20
18
|
)
|
21
19
|
|
22
|
-
from .
|
23
|
-
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
|
24
24
|
|
25
25
|
if TYPE_CHECKING:
|
26
26
|
import requests.auth
|
27
27
|
|
28
|
-
from .
|
29
|
-
from .types import GenericTest
|
28
|
+
from schemathesis.schemas import APIOperation
|
30
29
|
|
31
30
|
DEFAULT_REFRESH_INTERVAL = 300
|
32
|
-
|
31
|
+
AuthStorageMark = Mark["AuthStorage"](attr_name="auth_storage")
|
33
32
|
Auth = TypeVar("Auth")
|
34
33
|
|
35
34
|
|
36
35
|
@dataclass
|
37
36
|
class AuthContext:
|
38
|
-
"""
|
37
|
+
"""Runtime context passed to authentication providers during token generation.
|
38
|
+
|
39
|
+
Provides access to the current API operation and application instance when
|
40
|
+
auth providers need operation-specific tokens or application state.
|
41
|
+
|
42
|
+
Example:
|
43
|
+
```python
|
44
|
+
@schemathesis.auth()
|
45
|
+
class ContextAwareAuth:
|
46
|
+
def get(self, case, context):
|
47
|
+
# Access operation details
|
48
|
+
if "/admin/" in context.operation.path:
|
49
|
+
return self.get_admin_token()
|
50
|
+
else:
|
51
|
+
return self.get_user_token()
|
52
|
+
|
53
|
+
def set(self, case, data, context):
|
54
|
+
case.headers = {"Authorization": f"Bearer {data}"}
|
55
|
+
```
|
39
56
|
|
40
|
-
:ivar APIOperation operation: API operation that is currently being processed.
|
41
|
-
:ivar app: Optional Python application if the WSGI / ASGI integration is used.
|
42
57
|
"""
|
43
58
|
|
44
59
|
operation: APIOperation
|
60
|
+
"""API operation currently being processed for authentication."""
|
45
61
|
app: Any | None
|
62
|
+
"""Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
|
63
|
+
|
64
|
+
__slots__ = ("operation", "app")
|
46
65
|
|
47
66
|
|
48
67
|
CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
@@ -50,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
|
50
69
|
|
51
70
|
@runtime_checkable
|
52
71
|
class AuthProvider(Generic[Auth], Protocol):
|
53
|
-
"""
|
72
|
+
"""Protocol for implementing custom authentication in API tests."""
|
54
73
|
|
55
|
-
def get(self, case: Case,
|
56
|
-
"""
|
74
|
+
def get(self, case: Case, ctx: AuthContext) -> Auth | None:
|
75
|
+
"""Obtain authentication data for the test case.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
case: Generated test case requiring authentication.
|
79
|
+
ctx: Authentication state and configuration.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Authentication data (e.g., token, credentials) or `None`.
|
57
83
|
|
58
|
-
:param Case case: Generated test case.
|
59
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
60
|
-
:return: Any authentication data you find useful for your use case. For example, it could be an access token.
|
61
84
|
"""
|
62
85
|
|
63
|
-
def set(self, case: Case, data: Auth,
|
64
|
-
"""
|
86
|
+
def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
|
87
|
+
"""Apply authentication data to the test case.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
case: Test case to modify.
|
91
|
+
data: Authentication data from the `get` method.
|
92
|
+
ctx: Authentication state and configuration.
|
65
93
|
|
66
|
-
:param Optional[Auth] data: Authentication data you got from the ``get`` method.
|
67
|
-
:param Case case: Generated test case.
|
68
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
69
94
|
"""
|
70
95
|
|
71
96
|
|
@@ -111,7 +136,7 @@ class CachingAuthProvider(Generic[Auth]):
|
|
111
136
|
# Another thread updated the cache
|
112
137
|
return cache_entry.data
|
113
138
|
# We know that optional auth is possible only inside a higher-level wrapper
|
114
|
-
data: Auth =
|
139
|
+
data: Auth = self.provider.get(case, context) # type: ignore[assignment]
|
115
140
|
self._set_cache_entry(data, case, context)
|
116
141
|
return data
|
117
142
|
return cache_entry.data
|
@@ -152,8 +177,7 @@ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
|
|
152
177
|
class FilterableRegisterAuth(Protocol):
|
153
178
|
"""Protocol that adds filters to the return value of `register`."""
|
154
179
|
|
155
|
-
def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
156
|
-
pass
|
180
|
+
def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
|
157
181
|
|
158
182
|
def apply_to(
|
159
183
|
self,
|
@@ -165,8 +189,7 @@ class FilterableRegisterAuth(Protocol):
|
|
165
189
|
method_regex: str | None = None,
|
166
190
|
path: FilterValue | None = None,
|
167
191
|
path_regex: str | None = None,
|
168
|
-
) -> FilterableRegisterAuth:
|
169
|
-
pass
|
192
|
+
) -> FilterableRegisterAuth: ...
|
170
193
|
|
171
194
|
def skip_for(
|
172
195
|
self,
|
@@ -178,15 +201,13 @@ class FilterableRegisterAuth(Protocol):
|
|
178
201
|
method_regex: str | None = None,
|
179
202
|
path: FilterValue | None = None,
|
180
203
|
path_regex: str | None = None,
|
181
|
-
) -> FilterableRegisterAuth:
|
182
|
-
pass
|
204
|
+
) -> FilterableRegisterAuth: ...
|
183
205
|
|
184
206
|
|
185
207
|
class FilterableApplyAuth(Protocol):
|
186
208
|
"""Protocol that adds filters to the return value of `apply`."""
|
187
209
|
|
188
|
-
def __call__(self, test:
|
189
|
-
pass
|
210
|
+
def __call__(self, test: Callable) -> Callable: ...
|
190
211
|
|
191
212
|
def apply_to(
|
192
213
|
self,
|
@@ -198,8 +219,7 @@ class FilterableApplyAuth(Protocol):
|
|
198
219
|
method_regex: str | None = None,
|
199
220
|
path: FilterValue | None = None,
|
200
221
|
path_regex: str | None = None,
|
201
|
-
) -> FilterableApplyAuth:
|
202
|
-
pass
|
222
|
+
) -> FilterableApplyAuth: ...
|
203
223
|
|
204
224
|
def skip_for(
|
205
225
|
self,
|
@@ -211,8 +231,7 @@ class FilterableApplyAuth(Protocol):
|
|
211
231
|
method_regex: str | None = None,
|
212
232
|
path: FilterValue | None = None,
|
213
233
|
path_regex: str | None = None,
|
214
|
-
) -> FilterableApplyAuth:
|
215
|
-
pass
|
234
|
+
) -> FilterableApplyAuth: ...
|
216
235
|
|
217
236
|
|
218
237
|
class FilterableRequestsAuth(Protocol):
|
@@ -228,8 +247,7 @@ class FilterableRequestsAuth(Protocol):
|
|
228
247
|
method_regex: str | None = None,
|
229
248
|
path: FilterValue | None = None,
|
230
249
|
path_regex: str | None = None,
|
231
|
-
) -> FilterableRequestsAuth:
|
232
|
-
pass
|
250
|
+
) -> FilterableRequestsAuth: ...
|
233
251
|
|
234
252
|
def skip_for(
|
235
253
|
self,
|
@@ -241,8 +259,7 @@ class FilterableRequestsAuth(Protocol):
|
|
241
259
|
method_regex: str | None = None,
|
242
260
|
path: FilterValue | None = None,
|
243
261
|
path_regex: str | None = None,
|
244
|
-
) -> FilterableRequestsAuth:
|
245
|
-
pass
|
262
|
+
) -> FilterableRequestsAuth: ...
|
246
263
|
|
247
264
|
|
248
265
|
@dataclass
|
@@ -254,7 +271,7 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
254
271
|
|
255
272
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
256
273
|
if self.filter_set.match(context):
|
257
|
-
return
|
274
|
+
return self.provider.get(case, context)
|
258
275
|
return None
|
259
276
|
|
260
277
|
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
@@ -278,8 +295,7 @@ class AuthStorage(Generic[Auth]):
|
|
278
295
|
*,
|
279
296
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
280
297
|
cache_by_key: CacheKeyFunction | None = None,
|
281
|
-
) -> FilterableRegisterAuth:
|
282
|
-
pass
|
298
|
+
) -> FilterableRegisterAuth: ...
|
283
299
|
|
284
300
|
@overload
|
285
301
|
def __call__(
|
@@ -288,8 +304,7 @@ class AuthStorage(Generic[Auth]):
|
|
288
304
|
*,
|
289
305
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
290
306
|
cache_by_key: CacheKeyFunction | None = None,
|
291
|
-
) -> FilterableApplyAuth:
|
292
|
-
pass
|
307
|
+
) -> FilterableApplyAuth: ...
|
293
308
|
|
294
309
|
def __call__(
|
295
310
|
self,
|
@@ -300,15 +315,14 @@ class AuthStorage(Generic[Auth]):
|
|
300
315
|
) -> FilterableRegisterAuth | FilterableApplyAuth:
|
301
316
|
if provider_class is not None:
|
302
317
|
return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
303
|
-
return self.
|
318
|
+
return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
304
319
|
|
305
320
|
def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
|
306
321
|
"""Use `requests` auth instance as an auth provider."""
|
307
322
|
filter_set = FilterSet()
|
308
323
|
self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
|
309
324
|
|
310
|
-
class _FilterableRequestsAuth:
|
311
|
-
pass
|
325
|
+
class _FilterableRequestsAuth: ...
|
312
326
|
|
313
327
|
attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
|
314
328
|
attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
|
@@ -325,8 +339,9 @@ class AuthStorage(Generic[Auth]):
|
|
325
339
|
) -> None:
|
326
340
|
if not issubclass(provider_class, AuthProvider):
|
327
341
|
raise TypeError(
|
328
|
-
f"`{provider_class.__name__}`
|
329
|
-
f"
|
342
|
+
f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
|
343
|
+
f"Auth providers must have `get` and `set` methods. "
|
344
|
+
f"See `schemathesis.AuthProvider` documentation for examples."
|
330
345
|
)
|
331
346
|
provider: AuthProvider
|
332
347
|
# Apply caching if desired
|
@@ -345,30 +360,12 @@ class AuthStorage(Generic[Auth]):
|
|
345
360
|
provider = SelectiveAuthProvider(provider, filter_set)
|
346
361
|
self.providers.append(provider)
|
347
362
|
|
348
|
-
def
|
363
|
+
def auth(
|
349
364
|
self,
|
350
365
|
*,
|
351
366
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
352
367
|
cache_by_key: CacheKeyFunction | None = None,
|
353
368
|
) -> FilterableRegisterAuth:
|
354
|
-
"""Register a new auth provider.
|
355
|
-
|
356
|
-
.. code-block:: python
|
357
|
-
|
358
|
-
@schemathesis.auth()
|
359
|
-
class TokenAuth:
|
360
|
-
def get(self, context):
|
361
|
-
response = requests.post(
|
362
|
-
"https://example.schemathesis.io/api/token/",
|
363
|
-
json={"username": "demo", "password": "test"},
|
364
|
-
)
|
365
|
-
data = response.json()
|
366
|
-
return data["access_token"]
|
367
|
-
|
368
|
-
def set(self, case, data, context):
|
369
|
-
# Modify `case` the way you need
|
370
|
-
case.headers = {"Authorization": f"Bearer {data}"}
|
371
|
-
"""
|
372
369
|
filter_set = FilterSet()
|
373
370
|
|
374
371
|
def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
@@ -399,27 +396,13 @@ class AuthStorage(Generic[Auth]):
|
|
399
396
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
400
397
|
cache_by_key: CacheKeyFunction | None = None,
|
401
398
|
) -> FilterableApplyAuth:
|
402
|
-
"""Register auth provider only on one test function.
|
403
|
-
|
404
|
-
:param Type[AuthProvider] provider_class: Authentication provider class.
|
405
|
-
:param Optional[int] refresh_interval: Cache duration in seconds.
|
406
|
-
|
407
|
-
.. code-block:: python
|
408
|
-
|
409
|
-
class Auth:
|
410
|
-
...
|
411
|
-
|
412
|
-
|
413
|
-
@schema.auth(Auth)
|
414
|
-
@schema.parametrize()
|
415
|
-
def test_api(case):
|
416
|
-
...
|
417
|
-
|
418
|
-
"""
|
419
399
|
filter_set = FilterSet()
|
420
400
|
|
421
|
-
def wrapper(test:
|
422
|
-
|
401
|
+
def wrapper(test: Callable) -> Callable:
|
402
|
+
if AuthStorageMark.is_set(test):
|
403
|
+
raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `apply`.")
|
404
|
+
auth_storage = self.__class__()
|
405
|
+
AuthStorageMark.set(test, auth_storage)
|
423
406
|
auth_storage._set_provider(
|
424
407
|
provider_class=provider_class,
|
425
408
|
refresh_interval=refresh_interval,
|
@@ -433,46 +416,18 @@ class AuthStorage(Generic[Auth]):
|
|
433
416
|
|
434
417
|
return wrapper # type: ignore[return-value]
|
435
418
|
|
436
|
-
@classmethod
|
437
|
-
def add_auth_storage(cls, test: GenericTest) -> AuthStorage:
|
438
|
-
"""Attach a new auth storage instance to the test if it is not already present."""
|
439
|
-
if not hasattr(test, AUTH_STORAGE_ATTRIBUTE_NAME):
|
440
|
-
setattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, cls())
|
441
|
-
else:
|
442
|
-
raise UsageError(f"`{test.__name__}` has already been decorated with `apply`.")
|
443
|
-
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME)
|
444
|
-
|
445
419
|
def set(self, case: Case, context: AuthContext) -> None:
|
446
420
|
"""Set authentication data on a generated test case."""
|
447
421
|
if not self.is_defined:
|
448
|
-
raise
|
422
|
+
raise IncorrectUsage("No auth provider is defined.")
|
449
423
|
for provider in self.providers:
|
450
|
-
data: Auth | None =
|
424
|
+
data: Auth | None = provider.get(case, context)
|
451
425
|
if data is not None:
|
452
426
|
provider.set(case, data, context)
|
453
427
|
case._has_explicit_auth = True
|
454
428
|
break
|
455
429
|
|
456
430
|
|
457
|
-
def _provider_get(auth_provider: AuthProvider, case: Case, context: AuthContext) -> Auth | None:
|
458
|
-
# A shim to provide a compatibility layer between previously used convention for `AuthProvider.get`
|
459
|
-
# where it used to accept a single `context` argument
|
460
|
-
method = auth_provider.get
|
461
|
-
parameters = inspect.signature(method).parameters
|
462
|
-
if len(parameters) == 1:
|
463
|
-
# Old calling convention
|
464
|
-
warnings.warn(
|
465
|
-
"The method 'get' of your AuthProvider is using the old calling convention, "
|
466
|
-
"which is deprecated and will be removed in Schemathesis 4.0. "
|
467
|
-
"Please update it to accept both 'case' and 'context' as arguments.",
|
468
|
-
DeprecationWarning,
|
469
|
-
stacklevel=1,
|
470
|
-
)
|
471
|
-
return method(context) # type: ignore
|
472
|
-
# New calling convention
|
473
|
-
return method(case, context)
|
474
|
-
|
475
|
-
|
476
431
|
def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
|
477
432
|
"""Set authentication data on this case.
|
478
433
|
|
@@ -486,12 +441,46 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
486
441
|
GLOBAL_AUTH_STORAGE.set(case, context)
|
487
442
|
|
488
443
|
|
489
|
-
def get_auth_storage_from_test(test: GenericTest) -> AuthStorage | None:
|
490
|
-
"""Extract the currently attached auth storage from a test function."""
|
491
|
-
return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, None)
|
492
|
-
|
493
|
-
|
494
444
|
# Global auth API
|
495
445
|
GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
|
496
|
-
register = GLOBAL_AUTH_STORAGE.register
|
497
446
|
unregister = GLOBAL_AUTH_STORAGE.unregister
|
447
|
+
|
448
|
+
|
449
|
+
def auth(
|
450
|
+
*,
|
451
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
452
|
+
cache_by_key: CacheKeyFunction | None = None,
|
453
|
+
) -> FilterableRegisterAuth:
|
454
|
+
"""Register a dynamic authentication provider for APIs with expiring tokens.
|
455
|
+
|
456
|
+
Args:
|
457
|
+
refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
|
458
|
+
cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
|
459
|
+
|
460
|
+
Example:
|
461
|
+
```python
|
462
|
+
import schemathesis
|
463
|
+
import requests
|
464
|
+
|
465
|
+
@schemathesis.auth()
|
466
|
+
class TokenAuth:
|
467
|
+
def get(self, case, context):
|
468
|
+
\"\"\"Fetch fresh authentication token\"\"\"
|
469
|
+
response = requests.post(
|
470
|
+
"http://localhost:8000/auth/token",
|
471
|
+
json={"username": "demo", "password": "test"}
|
472
|
+
)
|
473
|
+
return response.json()["access_token"]
|
474
|
+
|
475
|
+
def set(self, case, data, context):
|
476
|
+
\"\"\"Apply token to test case headers\"\"\"
|
477
|
+
case.headers = case.headers or {}
|
478
|
+
case.headers["Authorization"] = f"Bearer {data}"
|
479
|
+
```
|
480
|
+
|
481
|
+
"""
|
482
|
+
return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
483
|
+
|
484
|
+
|
485
|
+
auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
|
486
|
+
auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]
|