schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- 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 +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -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} +59 -175
- schemathesis/cli/constants.py +5 -58
- 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} +37 -16
- 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 -7
- 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 +315 -0
- schemathesis/core/fs.py +19 -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/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -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 +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 +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -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/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -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} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- 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 +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- 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} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- 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 -936
- 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 -56
- 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/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- 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 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- 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 -104
- schemathesis/runner/impl/core.py +0 -1246
- 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/loaders.py +0 -708
- 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/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- 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.7.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.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,100 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING
|
4
|
-
|
5
|
-
from ..exceptions import CheckFailed, get_grouped_exception
|
6
|
-
from ..internal.checks import CheckContext
|
7
|
-
|
8
|
-
if TYPE_CHECKING:
|
9
|
-
from ..failures import FailureContext
|
10
|
-
from ..internal.checks import CheckFunction
|
11
|
-
from ..models import Case
|
12
|
-
from ..transports.responses import GenericResponse
|
13
|
-
from .context import RunnerContext
|
14
|
-
|
15
|
-
|
16
|
-
def validate_response(
|
17
|
-
*,
|
18
|
-
response: GenericResponse,
|
19
|
-
case: Case,
|
20
|
-
runner_ctx: RunnerContext,
|
21
|
-
check_ctx: CheckContext,
|
22
|
-
checks: tuple[CheckFunction, ...],
|
23
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
24
|
-
max_response_time: int | None = None,
|
25
|
-
) -> None:
|
26
|
-
"""Validate the response against the provided checks."""
|
27
|
-
from .._compat import MultipleFailures
|
28
|
-
from ..checks import _make_max_response_time_failure_message
|
29
|
-
from ..failures import ResponseTimeExceeded
|
30
|
-
from ..models import Check, Status
|
31
|
-
|
32
|
-
exceptions: list[CheckFailed | AssertionError] = []
|
33
|
-
check_results = runner_ctx.checks_for_step
|
34
|
-
|
35
|
-
def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
|
36
|
-
exceptions.append(exc)
|
37
|
-
if runner_ctx.is_seen_in_suite(exc):
|
38
|
-
return
|
39
|
-
failed_check = Check(
|
40
|
-
name=name,
|
41
|
-
value=Status.failure,
|
42
|
-
response=response,
|
43
|
-
elapsed=response.elapsed.total_seconds(),
|
44
|
-
example=copied_case,
|
45
|
-
message=message,
|
46
|
-
context=context,
|
47
|
-
request=None,
|
48
|
-
)
|
49
|
-
runner_ctx.add_failed_check(failed_check)
|
50
|
-
check_results.append(failed_check)
|
51
|
-
runner_ctx.mark_as_seen_in_suite(exc)
|
52
|
-
|
53
|
-
def _on_passed(_name: str, _case: Case) -> None:
|
54
|
-
passed_check = Check(
|
55
|
-
name=_name,
|
56
|
-
value=Status.success,
|
57
|
-
response=response,
|
58
|
-
elapsed=response.elapsed.total_seconds(),
|
59
|
-
example=_case,
|
60
|
-
request=None,
|
61
|
-
)
|
62
|
-
check_results.append(passed_check)
|
63
|
-
|
64
|
-
for check in tuple(checks) + tuple(additional_checks):
|
65
|
-
name = check.__name__
|
66
|
-
copied_case = case.partial_deepcopy()
|
67
|
-
try:
|
68
|
-
skip_check = check(check_ctx, response, copied_case)
|
69
|
-
if not skip_check:
|
70
|
-
_on_passed(name, copied_case)
|
71
|
-
except CheckFailed as exc:
|
72
|
-
if runner_ctx.is_seen_in_run(exc):
|
73
|
-
continue
|
74
|
-
_on_failure(exc, str(exc), exc.context)
|
75
|
-
except AssertionError as exc:
|
76
|
-
if runner_ctx.is_seen_in_run(exc):
|
77
|
-
continue
|
78
|
-
_on_failure(exc, str(exc) or f"Custom check failed: `{name}`", None)
|
79
|
-
except MultipleFailures as exc:
|
80
|
-
for subexc in exc.exceptions:
|
81
|
-
if runner_ctx.is_seen_in_run(subexc):
|
82
|
-
continue
|
83
|
-
_on_failure(subexc, str(subexc), subexc.context)
|
84
|
-
|
85
|
-
if max_response_time:
|
86
|
-
elapsed_time = response.elapsed.total_seconds() * 1000
|
87
|
-
if elapsed_time > max_response_time:
|
88
|
-
message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
|
89
|
-
context = ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time)
|
90
|
-
try:
|
91
|
-
raise AssertionError(message)
|
92
|
-
except AssertionError as _exc:
|
93
|
-
if not runner_ctx.is_seen_in_run(_exc):
|
94
|
-
_on_failure(_exc, message, context)
|
95
|
-
else:
|
96
|
-
_on_passed("max_response_time", case)
|
97
|
-
|
98
|
-
# Raise a grouped exception so Hypothesis can properly deduplicate it against the other failures
|
99
|
-
if exceptions:
|
100
|
-
raise get_grouped_exception(case.operation.verbose_name, *exceptions)(causes=tuple(exceptions))
|
schemathesis/targets.py
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Callable
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from .models import Case
|
8
|
-
from .transports.responses import GenericResponse
|
9
|
-
|
10
|
-
|
11
|
-
@dataclass
|
12
|
-
class TargetContext:
|
13
|
-
"""Context for targeted testing.
|
14
|
-
|
15
|
-
:ivar Case case: Generated example that is being processed.
|
16
|
-
:ivar GenericResponse response: API response.
|
17
|
-
:ivar float response_time: API response time.
|
18
|
-
"""
|
19
|
-
|
20
|
-
case: Case
|
21
|
-
response: GenericResponse
|
22
|
-
response_time: float
|
23
|
-
|
24
|
-
|
25
|
-
def response_time(context: TargetContext) -> float:
|
26
|
-
return context.response_time
|
27
|
-
|
28
|
-
|
29
|
-
Target = Callable[[TargetContext], float]
|
30
|
-
DEFAULT_TARGETS = ()
|
31
|
-
OPTIONAL_TARGETS = (response_time,)
|
32
|
-
ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class TargetMetricCollector:
|
37
|
-
"""Collect multiple observations for target metrics."""
|
38
|
-
|
39
|
-
targets: list[Target]
|
40
|
-
observations: dict[str, list[int | float]] = field(init=False)
|
41
|
-
|
42
|
-
def __post_init__(self) -> None:
|
43
|
-
self.observations = {target.__name__: [] for target in self.targets}
|
44
|
-
|
45
|
-
def reset(self) -> None:
|
46
|
-
"""Reset all collected observations."""
|
47
|
-
for target in self.targets:
|
48
|
-
self.observations[target.__name__].clear()
|
49
|
-
|
50
|
-
def store(self, case: Case, response: GenericResponse) -> None:
|
51
|
-
"""Calculate target metrics & store them."""
|
52
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
53
|
-
for target in self.targets:
|
54
|
-
self.observations[target.__name__].append(target(context))
|
55
|
-
|
56
|
-
def maximize(self) -> None:
|
57
|
-
"""Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
|
58
|
-
import hypothesis
|
59
|
-
|
60
|
-
for target in self.targets:
|
61
|
-
# Currently aggregation is just a sum
|
62
|
-
metric = sum(self.observations[target.__name__])
|
63
|
-
hypothesis.target(metric, label=target.__name__)
|
64
|
-
|
65
|
-
|
66
|
-
def register(target: Target) -> Target:
|
67
|
-
"""Register a new testing target for schemathesis CLI.
|
68
|
-
|
69
|
-
:param target: A function that will be called to calculate a metric passed to ``hypothesis.target``.
|
70
|
-
"""
|
71
|
-
from . import cli
|
72
|
-
|
73
|
-
global ALL_TARGETS
|
74
|
-
|
75
|
-
ALL_TARGETS += (target,)
|
76
|
-
cli.TARGETS_TYPE.choices += (target.__name__,) # type: ignore
|
77
|
-
return target
|
@@ -1,359 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import base64
|
4
|
-
import inspect
|
5
|
-
import time
|
6
|
-
from contextlib import contextmanager
|
7
|
-
from dataclasses import dataclass
|
8
|
-
from datetime import timedelta
|
9
|
-
from inspect import iscoroutinefunction
|
10
|
-
from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
|
11
|
-
from urllib.parse import urlparse
|
12
|
-
|
13
|
-
from .. import failures
|
14
|
-
from .._dependency_versions import IS_WERKZEUG_ABOVE_3
|
15
|
-
from ..constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET
|
16
|
-
from ..exceptions import get_timeout_error
|
17
|
-
from ..serializers import SerializerContext
|
18
|
-
from ..types import Cookies, NotSet, RequestCert
|
19
|
-
|
20
|
-
if TYPE_CHECKING:
|
21
|
-
import requests
|
22
|
-
import werkzeug
|
23
|
-
from _typeshed.wsgi import WSGIApplication
|
24
|
-
from starlette_testclient._testclient import ASGI2App, ASGI3App
|
25
|
-
|
26
|
-
from ..models import Case
|
27
|
-
from .responses import WSGIResponse
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class RequestConfig:
|
32
|
-
timeout: int | None = None
|
33
|
-
tls_verify: bool | str = True
|
34
|
-
proxy: str | None = None
|
35
|
-
cert: RequestCert | None = None
|
36
|
-
|
37
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
38
|
-
|
39
|
-
@property
|
40
|
-
def prepared_timeout(self) -> float | None:
|
41
|
-
return prepare_timeout(self.timeout)
|
42
|
-
|
43
|
-
|
44
|
-
def serialize_payload(payload: bytes) -> str:
|
45
|
-
return base64.b64encode(payload).decode()
|
46
|
-
|
47
|
-
|
48
|
-
def deserialize_payload(data: str | None) -> bytes | None:
|
49
|
-
if data is None:
|
50
|
-
return None
|
51
|
-
return base64.b64decode(data)
|
52
|
-
|
53
|
-
|
54
|
-
def get(app: Any) -> Transport:
|
55
|
-
"""Get transport to send the data to the application."""
|
56
|
-
if app is None:
|
57
|
-
return RequestsTransport()
|
58
|
-
if iscoroutinefunction(app) or (
|
59
|
-
hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
|
60
|
-
):
|
61
|
-
return ASGITransport(app=app)
|
62
|
-
return WSGITransport(app=app)
|
63
|
-
|
64
|
-
|
65
|
-
S = TypeVar("S", contravariant=True)
|
66
|
-
R = TypeVar("R", covariant=True)
|
67
|
-
|
68
|
-
|
69
|
-
class Transport(Protocol[S, R]):
|
70
|
-
def serialize_case(
|
71
|
-
self,
|
72
|
-
case: Case,
|
73
|
-
*,
|
74
|
-
base_url: str | None = None,
|
75
|
-
headers: dict[str, Any] | None = None,
|
76
|
-
params: dict[str, Any] | None = None,
|
77
|
-
cookies: dict[str, Any] | None = None,
|
78
|
-
) -> dict[str, Any]:
|
79
|
-
raise NotImplementedError
|
80
|
-
|
81
|
-
def send(
|
82
|
-
self,
|
83
|
-
case: Case,
|
84
|
-
*,
|
85
|
-
session: S | None = None,
|
86
|
-
base_url: str | None = None,
|
87
|
-
headers: dict[str, Any] | None = None,
|
88
|
-
params: dict[str, Any] | None = None,
|
89
|
-
cookies: dict[str, Any] | None = None,
|
90
|
-
**kwargs: Any,
|
91
|
-
) -> R:
|
92
|
-
raise NotImplementedError
|
93
|
-
|
94
|
-
|
95
|
-
class RequestsTransport:
|
96
|
-
def serialize_case(
|
97
|
-
self,
|
98
|
-
case: Case,
|
99
|
-
*,
|
100
|
-
base_url: str | None = None,
|
101
|
-
headers: dict[str, Any] | None = None,
|
102
|
-
params: dict[str, Any] | None = None,
|
103
|
-
cookies: dict[str, Any] | None = None,
|
104
|
-
) -> dict[str, Any]:
|
105
|
-
final_headers = case._get_headers(headers)
|
106
|
-
media_type: str | None
|
107
|
-
if case.body is not NOT_SET and case.media_type is None:
|
108
|
-
media_type = case.operation._get_default_media_type()
|
109
|
-
else:
|
110
|
-
media_type = case.media_type
|
111
|
-
if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
|
112
|
-
# `requests` will handle multipart form headers with the proper `boundary` value.
|
113
|
-
if "content-type" not in final_headers:
|
114
|
-
final_headers["Content-Type"] = media_type
|
115
|
-
url = case._get_url(base_url)
|
116
|
-
serializer = case._get_serializer(media_type)
|
117
|
-
if serializer is not None and not isinstance(case.body, NotSet):
|
118
|
-
context = SerializerContext(case=case)
|
119
|
-
extra = serializer.as_requests(context, case._get_body())
|
120
|
-
else:
|
121
|
-
extra = {}
|
122
|
-
if case._auth is not None:
|
123
|
-
extra["auth"] = case._auth
|
124
|
-
additional_headers = extra.pop("headers", None)
|
125
|
-
if additional_headers:
|
126
|
-
# Additional headers, needed for the serializer
|
127
|
-
for key, value in additional_headers.items():
|
128
|
-
final_headers.setdefault(key, value)
|
129
|
-
data = {
|
130
|
-
"method": case.method,
|
131
|
-
"url": url,
|
132
|
-
"cookies": case.cookies,
|
133
|
-
"headers": final_headers,
|
134
|
-
"params": case.query,
|
135
|
-
**extra,
|
136
|
-
}
|
137
|
-
if params is not None:
|
138
|
-
_merge_dict_to(data, "params", params)
|
139
|
-
if cookies is not None:
|
140
|
-
_merge_dict_to(data, "cookies", cookies)
|
141
|
-
return data
|
142
|
-
|
143
|
-
def send(
|
144
|
-
self,
|
145
|
-
case: Case,
|
146
|
-
*,
|
147
|
-
session: requests.Session | None = None,
|
148
|
-
base_url: str | None = None,
|
149
|
-
headers: dict[str, Any] | None = None,
|
150
|
-
params: dict[str, Any] | None = None,
|
151
|
-
cookies: dict[str, Any] | None = None,
|
152
|
-
**kwargs: Any,
|
153
|
-
) -> requests.Response:
|
154
|
-
import requests
|
155
|
-
from urllib3.exceptions import ReadTimeoutError
|
156
|
-
|
157
|
-
data = self.serialize_case(case, base_url=base_url, headers=headers, params=params, cookies=cookies)
|
158
|
-
data.update(kwargs)
|
159
|
-
data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
160
|
-
if session is None:
|
161
|
-
validate_vanilla_requests_kwargs(data)
|
162
|
-
session = requests.Session()
|
163
|
-
close_session = True
|
164
|
-
else:
|
165
|
-
close_session = False
|
166
|
-
verify = data.get("verify", True)
|
167
|
-
try:
|
168
|
-
with case.operation.schema.ratelimit():
|
169
|
-
response = session.request(**data) # type: ignore
|
170
|
-
except (requests.Timeout, requests.ConnectionError) as exc:
|
171
|
-
if isinstance(exc, requests.ConnectionError):
|
172
|
-
if not isinstance(exc.args[0], ReadTimeoutError):
|
173
|
-
raise
|
174
|
-
req = requests.Request(
|
175
|
-
method=data["method"].upper(),
|
176
|
-
url=data["url"],
|
177
|
-
headers=data["headers"],
|
178
|
-
files=data.get("files"),
|
179
|
-
data=data.get("data") or {},
|
180
|
-
json=data.get("json"),
|
181
|
-
params=data.get("params") or {},
|
182
|
-
auth=data.get("auth"),
|
183
|
-
cookies=data["cookies"],
|
184
|
-
hooks=data.get("hooks"),
|
185
|
-
)
|
186
|
-
request = session.prepare_request(req)
|
187
|
-
else:
|
188
|
-
request = cast(requests.PreparedRequest, exc.request)
|
189
|
-
timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
|
190
|
-
code_message = case._get_code_message(case.operation.schema.code_sample_style, request, verify=verify)
|
191
|
-
message = f"The server failed to respond within the specified limit of {timeout:.2f}ms"
|
192
|
-
raise get_timeout_error(case.operation.verbose_name, timeout)(
|
193
|
-
f"\n\n1. {failures.RequestTimeout.title}\n\n{message}\n\n{code_message}",
|
194
|
-
context=failures.RequestTimeout(message=message, timeout=timeout),
|
195
|
-
) from None
|
196
|
-
response.verify = verify # type: ignore[attr-defined]
|
197
|
-
response._session = session # type: ignore[attr-defined]
|
198
|
-
if close_session:
|
199
|
-
session.close()
|
200
|
-
return response
|
201
|
-
|
202
|
-
|
203
|
-
def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
|
204
|
-
original = data[data_key] or {}
|
205
|
-
for key, value in new.items():
|
206
|
-
original[key] = value
|
207
|
-
data[data_key] = original
|
208
|
-
|
209
|
-
|
210
|
-
def prepare_timeout(timeout: int | None) -> float | None:
|
211
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
212
|
-
output: int | float | None = timeout
|
213
|
-
if timeout is not None:
|
214
|
-
output = timeout / 1000
|
215
|
-
return output
|
216
|
-
|
217
|
-
|
218
|
-
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
219
|
-
"""Check arguments for `requests.Session.request`.
|
220
|
-
|
221
|
-
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
222
|
-
`requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
|
223
|
-
"""
|
224
|
-
url = data["url"]
|
225
|
-
if not urlparse(url).netloc:
|
226
|
-
stack = inspect.stack()
|
227
|
-
method_name = "call"
|
228
|
-
for frame in stack[1:]:
|
229
|
-
if frame.function == "call_and_validate":
|
230
|
-
method_name = "call_and_validate"
|
231
|
-
break
|
232
|
-
raise RuntimeError(
|
233
|
-
"The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
|
234
|
-
f"Pass `base_url` either to the `schemathesis.from_*` loader or to the `Case.{method_name}`.\n"
|
235
|
-
f"If you use the ASGI integration, please supply your test client "
|
236
|
-
f"as the `session` argument to `call`.\nURL: {url}"
|
237
|
-
)
|
238
|
-
|
239
|
-
|
240
|
-
@dataclass
|
241
|
-
class ASGITransport(RequestsTransport):
|
242
|
-
app: ASGI2App | ASGI3App
|
243
|
-
|
244
|
-
def send(
|
245
|
-
self,
|
246
|
-
case: Case,
|
247
|
-
*,
|
248
|
-
session: requests.Session | None = None,
|
249
|
-
base_url: str | None = None,
|
250
|
-
headers: dict[str, Any] | None = None,
|
251
|
-
params: dict[str, Any] | None = None,
|
252
|
-
cookies: dict[str, Any] | None = None,
|
253
|
-
**kwargs: Any,
|
254
|
-
) -> requests.Response:
|
255
|
-
from starlette_testclient import TestClient as ASGIClient
|
256
|
-
|
257
|
-
if base_url is None:
|
258
|
-
base_url = case.get_full_base_url()
|
259
|
-
with ASGIClient(self.app) as client:
|
260
|
-
return super().send(
|
261
|
-
case, session=client, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
|
262
|
-
)
|
263
|
-
|
264
|
-
|
265
|
-
@dataclass
|
266
|
-
class WSGITransport:
|
267
|
-
app: WSGIApplication
|
268
|
-
|
269
|
-
def serialize_case(
|
270
|
-
self,
|
271
|
-
case: Case,
|
272
|
-
*,
|
273
|
-
base_url: str | None = None,
|
274
|
-
headers: dict[str, Any] | None = None,
|
275
|
-
params: dict[str, Any] | None = None,
|
276
|
-
cookies: dict[str, Any] | None = None,
|
277
|
-
) -> dict[str, Any]:
|
278
|
-
final_headers = case._get_headers(headers)
|
279
|
-
media_type: str | None
|
280
|
-
if case.body is not NOT_SET and case.media_type is None:
|
281
|
-
media_type = case.operation._get_default_media_type()
|
282
|
-
else:
|
283
|
-
media_type = case.media_type
|
284
|
-
if media_type and not isinstance(case.body, NotSet):
|
285
|
-
# If we need to send a payload, then the Content-Type header should be set
|
286
|
-
final_headers["Content-Type"] = media_type
|
287
|
-
extra: dict[str, Any]
|
288
|
-
serializer = case._get_serializer(media_type)
|
289
|
-
if serializer is not None and not isinstance(case.body, NotSet):
|
290
|
-
context = SerializerContext(case=case)
|
291
|
-
extra = serializer.as_werkzeug(context, case._get_body())
|
292
|
-
else:
|
293
|
-
extra = {}
|
294
|
-
data = {
|
295
|
-
"method": case.method,
|
296
|
-
"path": case.operation.schema.get_full_path(case.formatted_path),
|
297
|
-
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
298
|
-
"headers": dict(final_headers),
|
299
|
-
"query_string": case.query,
|
300
|
-
**extra,
|
301
|
-
}
|
302
|
-
if params is not None:
|
303
|
-
_merge_dict_to(data, "query_string", params)
|
304
|
-
return data
|
305
|
-
|
306
|
-
def send(
|
307
|
-
self,
|
308
|
-
case: Case,
|
309
|
-
*,
|
310
|
-
session: Any = None,
|
311
|
-
base_url: str | None = None,
|
312
|
-
headers: dict[str, Any] | None = None,
|
313
|
-
params: dict[str, Any] | None = None,
|
314
|
-
cookies: dict[str, Any] | None = None,
|
315
|
-
**kwargs: Any,
|
316
|
-
) -> WSGIResponse:
|
317
|
-
import requests
|
318
|
-
import werkzeug
|
319
|
-
|
320
|
-
from .responses import WSGIResponse
|
321
|
-
|
322
|
-
application = kwargs.pop("app", self.app) or self.app
|
323
|
-
data = self.serialize_case(case, headers=headers, params=params)
|
324
|
-
data.update(kwargs)
|
325
|
-
client = werkzeug.Client(application, WSGIResponse)
|
326
|
-
cookies = {**(case.cookies or {}), **(cookies or {})}
|
327
|
-
with cookie_handler(client, cookies), case.operation.schema.ratelimit():
|
328
|
-
start = time.monotonic()
|
329
|
-
response = client.open(**data)
|
330
|
-
elapsed = time.monotonic() - start
|
331
|
-
requests_kwargs = RequestsTransport().serialize_case(
|
332
|
-
case,
|
333
|
-
base_url=case.get_full_base_url(),
|
334
|
-
headers=headers,
|
335
|
-
params=params,
|
336
|
-
cookies=cookies,
|
337
|
-
)
|
338
|
-
response.request = requests.Request(**requests_kwargs).prepare()
|
339
|
-
response.elapsed = timedelta(seconds=elapsed)
|
340
|
-
return response
|
341
|
-
|
342
|
-
|
343
|
-
@contextmanager
|
344
|
-
def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
|
345
|
-
"""Set cookies required for a call."""
|
346
|
-
if not cookies:
|
347
|
-
yield
|
348
|
-
else:
|
349
|
-
for key, value in cookies.items():
|
350
|
-
if IS_WERKZEUG_ABOVE_3:
|
351
|
-
client.set_cookie(key=key, value=value, domain="localhost")
|
352
|
-
else:
|
353
|
-
client.set_cookie("localhost", key=key, value=value)
|
354
|
-
yield
|
355
|
-
for key in cookies:
|
356
|
-
if IS_WERKZEUG_ABOVE_3:
|
357
|
-
client.delete_cookie(key=key, domain="localhost")
|
358
|
-
else:
|
359
|
-
client.delete_cookie("localhost", key=key)
|
schemathesis/transports/asgi.py
DELETED
schemathesis/transports/auth.py
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING, Any
|
4
|
-
|
5
|
-
from ..constants import USER_AGENT
|
6
|
-
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from requests.auth import HTTPDigestAuth
|
9
|
-
|
10
|
-
from ..types import RawAuth
|
11
|
-
|
12
|
-
|
13
|
-
def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
|
14
|
-
from requests.auth import HTTPDigestAuth
|
15
|
-
|
16
|
-
if auth and auth_type == "digest":
|
17
|
-
return HTTPDigestAuth(*auth)
|
18
|
-
return auth
|
19
|
-
|
20
|
-
|
21
|
-
def prepare_wsgi_headers(headers: dict[str, Any] | None, auth: RawAuth | None, auth_type: str | None) -> dict[str, Any]:
|
22
|
-
headers = headers or {}
|
23
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
24
|
-
headers["User-Agent"] = USER_AGENT
|
25
|
-
wsgi_auth = get_wsgi_auth(auth, auth_type)
|
26
|
-
if wsgi_auth:
|
27
|
-
headers["Authorization"] = wsgi_auth
|
28
|
-
return headers
|
29
|
-
|
30
|
-
|
31
|
-
def get_wsgi_auth(auth: RawAuth | None, auth_type: str | None) -> str | None:
|
32
|
-
from requests.auth import _basic_auth_str
|
33
|
-
|
34
|
-
if auth:
|
35
|
-
if auth_type == "digest":
|
36
|
-
raise ValueError("Digest auth is not supported for WSGI apps")
|
37
|
-
return _basic_auth_str(*auth)
|
38
|
-
return None
|
@@ -1,36 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import re
|
4
|
-
from typing import Any
|
5
|
-
|
6
|
-
from ..constants import USER_AGENT
|
7
|
-
|
8
|
-
|
9
|
-
def setup_default_headers(kwargs: dict[str, Any]) -> None:
|
10
|
-
headers = kwargs.setdefault("headers", {})
|
11
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
12
|
-
kwargs["headers"]["User-Agent"] = USER_AGENT
|
13
|
-
|
14
|
-
|
15
|
-
def is_latin_1_encodable(value: str) -> bool:
|
16
|
-
"""Header values are encoded to latin-1 before sending."""
|
17
|
-
try:
|
18
|
-
value.encode("latin-1")
|
19
|
-
return True
|
20
|
-
except UnicodeEncodeError:
|
21
|
-
return False
|
22
|
-
|
23
|
-
|
24
|
-
# Adapted from http.client._is_illegal_header_value
|
25
|
-
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
26
|
-
|
27
|
-
|
28
|
-
def has_invalid_characters(name: str, value: str) -> bool:
|
29
|
-
from requests.exceptions import InvalidHeader
|
30
|
-
from requests.utils import check_header_validity
|
31
|
-
|
32
|
-
try:
|
33
|
-
check_header_validity((name, value))
|
34
|
-
return bool(INVALID_HEADER_RE.search(value))
|
35
|
-
except InvalidHeader:
|
36
|
-
return True
|
@@ -1,57 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import json
|
4
|
-
import sys
|
5
|
-
from typing import TYPE_CHECKING, Any, NoReturn, Union
|
6
|
-
|
7
|
-
from werkzeug.wrappers import Response as BaseResponse
|
8
|
-
|
9
|
-
from .._compat import JSONMixin
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
from datetime import timedelta
|
13
|
-
|
14
|
-
from httpx import Response as httpxResponse
|
15
|
-
from requests import PreparedRequest
|
16
|
-
from requests import Response as requestsResponse
|
17
|
-
|
18
|
-
|
19
|
-
class WSGIResponse(BaseResponse, JSONMixin):
|
20
|
-
# We store "requests" request to build a reproduction code
|
21
|
-
request: PreparedRequest
|
22
|
-
elapsed: timedelta
|
23
|
-
|
24
|
-
def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
|
25
|
-
# We don't need a werkzeug-specific exception when JSON parsing error happens
|
26
|
-
raise e
|
27
|
-
|
28
|
-
|
29
|
-
def get_payload(response: GenericResponse) -> str:
|
30
|
-
from httpx import Response as httpxResponse
|
31
|
-
from requests import Response as requestsResponse
|
32
|
-
|
33
|
-
if isinstance(response, (httpxResponse, requestsResponse)):
|
34
|
-
return response.text
|
35
|
-
return response.get_data(as_text=True)
|
36
|
-
|
37
|
-
|
38
|
-
def get_json(response: GenericResponse) -> Any:
|
39
|
-
from httpx import Response as httpxResponse
|
40
|
-
from requests import Response as requestsResponse
|
41
|
-
|
42
|
-
if isinstance(response, (httpxResponse, requestsResponse)):
|
43
|
-
return json.loads(response.text)
|
44
|
-
return response.json
|
45
|
-
|
46
|
-
|
47
|
-
def get_reason(status_code: int) -> str:
|
48
|
-
if sys.version_info < (3, 9) and status_code == 418:
|
49
|
-
# Python 3.8 does not have 418 status in the `HTTPStatus` enum
|
50
|
-
return "I'm a Teapot"
|
51
|
-
|
52
|
-
import http.client
|
53
|
-
|
54
|
-
return http.client.responses.get(status_code, "Unknown")
|
55
|
-
|
56
|
-
|
57
|
-
GenericResponse = Union["httpxResponse", "requestsResponse", WSGIResponse]
|