schemathesis 3.39.15__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 +238 -308
- 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.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.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 -712
- 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.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.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.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,369 +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 ..internal.copy import fast_deepcopy
|
18
|
-
from ..serializers import SerializerContext
|
19
|
-
from ..types import Cookies, NotSet, RequestCert
|
20
|
-
|
21
|
-
if TYPE_CHECKING:
|
22
|
-
import requests
|
23
|
-
import werkzeug
|
24
|
-
from _typeshed.wsgi import WSGIApplication
|
25
|
-
from starlette_testclient._testclient import ASGI2App, ASGI3App
|
26
|
-
|
27
|
-
from ..models import Case
|
28
|
-
from .responses import WSGIResponse
|
29
|
-
|
30
|
-
|
31
|
-
@dataclass
|
32
|
-
class RequestConfig:
|
33
|
-
timeout: int | None = None
|
34
|
-
tls_verify: bool | str = True
|
35
|
-
proxy: str | None = None
|
36
|
-
cert: RequestCert | None = None
|
37
|
-
|
38
|
-
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
39
|
-
|
40
|
-
@property
|
41
|
-
def prepared_timeout(self) -> float | None:
|
42
|
-
return prepare_timeout(self.timeout)
|
43
|
-
|
44
|
-
|
45
|
-
def serialize_payload(payload: bytes) -> str:
|
46
|
-
return base64.b64encode(payload).decode()
|
47
|
-
|
48
|
-
|
49
|
-
def deserialize_payload(data: str | None) -> bytes | None:
|
50
|
-
if data is None:
|
51
|
-
return None
|
52
|
-
return base64.b64decode(data)
|
53
|
-
|
54
|
-
|
55
|
-
def get(app: Any) -> Transport:
|
56
|
-
"""Get transport to send the data to the application."""
|
57
|
-
if app is None:
|
58
|
-
return RequestsTransport()
|
59
|
-
if iscoroutinefunction(app) or (
|
60
|
-
hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
|
61
|
-
):
|
62
|
-
return ASGITransport(app=app)
|
63
|
-
return WSGITransport(app=app)
|
64
|
-
|
65
|
-
|
66
|
-
S = TypeVar("S", contravariant=True)
|
67
|
-
R = TypeVar("R", covariant=True)
|
68
|
-
|
69
|
-
|
70
|
-
class Transport(Protocol[S, R]):
|
71
|
-
def serialize_case(
|
72
|
-
self,
|
73
|
-
case: Case,
|
74
|
-
*,
|
75
|
-
base_url: str | None = None,
|
76
|
-
headers: dict[str, Any] | None = None,
|
77
|
-
params: dict[str, Any] | None = None,
|
78
|
-
cookies: dict[str, Any] | None = None,
|
79
|
-
) -> dict[str, Any]:
|
80
|
-
raise NotImplementedError
|
81
|
-
|
82
|
-
def send(
|
83
|
-
self,
|
84
|
-
case: Case,
|
85
|
-
*,
|
86
|
-
session: S | None = None,
|
87
|
-
base_url: str | None = None,
|
88
|
-
headers: dict[str, Any] | None = None,
|
89
|
-
params: dict[str, Any] | None = None,
|
90
|
-
cookies: dict[str, Any] | None = None,
|
91
|
-
**kwargs: Any,
|
92
|
-
) -> R:
|
93
|
-
raise NotImplementedError
|
94
|
-
|
95
|
-
|
96
|
-
class RequestsTransport:
|
97
|
-
def serialize_case(
|
98
|
-
self,
|
99
|
-
case: Case,
|
100
|
-
*,
|
101
|
-
base_url: str | None = None,
|
102
|
-
headers: dict[str, Any] | None = None,
|
103
|
-
params: dict[str, Any] | None = None,
|
104
|
-
cookies: dict[str, Any] | None = None,
|
105
|
-
) -> dict[str, Any]:
|
106
|
-
final_headers = case._get_headers(headers)
|
107
|
-
media_type: str | None
|
108
|
-
if case.body is not NOT_SET and case.media_type is None:
|
109
|
-
media_type = case.operation._get_default_media_type()
|
110
|
-
else:
|
111
|
-
media_type = case.media_type
|
112
|
-
if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
|
113
|
-
# `requests` will handle multipart form headers with the proper `boundary` value.
|
114
|
-
if "content-type" not in final_headers:
|
115
|
-
final_headers["Content-Type"] = media_type
|
116
|
-
url = case._get_url(base_url)
|
117
|
-
serializer = case._get_serializer(media_type)
|
118
|
-
if serializer is not None and not isinstance(case.body, NotSet):
|
119
|
-
context = SerializerContext(case=case)
|
120
|
-
extra = serializer.as_requests(context, case._get_body())
|
121
|
-
else:
|
122
|
-
extra = {}
|
123
|
-
if case._auth is not None:
|
124
|
-
extra["auth"] = case._auth
|
125
|
-
additional_headers = extra.pop("headers", None)
|
126
|
-
if additional_headers:
|
127
|
-
# Additional headers, needed for the serializer
|
128
|
-
for key, value in additional_headers.items():
|
129
|
-
final_headers.setdefault(key, value)
|
130
|
-
|
131
|
-
p = case.query
|
132
|
-
|
133
|
-
# Replace empty dictionaries with empty strings, so the parameters actually present in the query string
|
134
|
-
if any(value == {} for value in (p or {}).values()):
|
135
|
-
p = fast_deepcopy(p)
|
136
|
-
for k, v in p.items():
|
137
|
-
if v == {}:
|
138
|
-
p[k] = ""
|
139
|
-
data = {
|
140
|
-
"method": case.method,
|
141
|
-
"url": url,
|
142
|
-
"cookies": case.cookies,
|
143
|
-
"headers": final_headers,
|
144
|
-
"params": p,
|
145
|
-
**extra,
|
146
|
-
}
|
147
|
-
if params is not None:
|
148
|
-
_merge_dict_to(data, "params", params)
|
149
|
-
if cookies is not None:
|
150
|
-
_merge_dict_to(data, "cookies", cookies)
|
151
|
-
return data
|
152
|
-
|
153
|
-
def send(
|
154
|
-
self,
|
155
|
-
case: Case,
|
156
|
-
*,
|
157
|
-
session: requests.Session | None = None,
|
158
|
-
base_url: str | None = None,
|
159
|
-
headers: dict[str, Any] | None = None,
|
160
|
-
params: dict[str, Any] | None = None,
|
161
|
-
cookies: dict[str, Any] | None = None,
|
162
|
-
**kwargs: Any,
|
163
|
-
) -> requests.Response:
|
164
|
-
import requests
|
165
|
-
from urllib3.exceptions import ReadTimeoutError
|
166
|
-
|
167
|
-
data = self.serialize_case(case, base_url=base_url, headers=headers, params=params, cookies=cookies)
|
168
|
-
data.update(kwargs)
|
169
|
-
data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
170
|
-
if session is None:
|
171
|
-
validate_vanilla_requests_kwargs(data)
|
172
|
-
session = requests.Session()
|
173
|
-
close_session = True
|
174
|
-
else:
|
175
|
-
close_session = False
|
176
|
-
verify = data.get("verify", True)
|
177
|
-
try:
|
178
|
-
with case.operation.schema.ratelimit():
|
179
|
-
response = session.request(**data) # type: ignore
|
180
|
-
except (requests.Timeout, requests.ConnectionError) as exc:
|
181
|
-
if isinstance(exc, requests.ConnectionError):
|
182
|
-
if not isinstance(exc.args[0], ReadTimeoutError):
|
183
|
-
raise
|
184
|
-
req = requests.Request(
|
185
|
-
method=data["method"].upper(),
|
186
|
-
url=data["url"],
|
187
|
-
headers=data["headers"],
|
188
|
-
files=data.get("files"),
|
189
|
-
data=data.get("data") or {},
|
190
|
-
json=data.get("json"),
|
191
|
-
params=data.get("params") or {},
|
192
|
-
auth=data.get("auth"),
|
193
|
-
cookies=data["cookies"],
|
194
|
-
hooks=data.get("hooks"),
|
195
|
-
)
|
196
|
-
request = session.prepare_request(req)
|
197
|
-
else:
|
198
|
-
request = cast(requests.PreparedRequest, exc.request)
|
199
|
-
timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
|
200
|
-
code_message = case._get_code_message(case.operation.schema.code_sample_style, request, verify=verify)
|
201
|
-
message = f"The server failed to respond within the specified limit of {timeout:.2f}ms"
|
202
|
-
raise get_timeout_error(case.operation.verbose_name, timeout)(
|
203
|
-
f"\n\n1. {failures.RequestTimeout.title}\n\n{message}\n\n{code_message}",
|
204
|
-
context=failures.RequestTimeout(message=message, timeout=timeout),
|
205
|
-
) from None
|
206
|
-
response.verify = verify # type: ignore[attr-defined]
|
207
|
-
response._session = session # type: ignore[attr-defined]
|
208
|
-
if close_session:
|
209
|
-
session.close()
|
210
|
-
return response
|
211
|
-
|
212
|
-
|
213
|
-
def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
|
214
|
-
original = data[data_key] or {}
|
215
|
-
for key, value in new.items():
|
216
|
-
original[key] = value
|
217
|
-
data[data_key] = original
|
218
|
-
|
219
|
-
|
220
|
-
def prepare_timeout(timeout: int | None) -> float | None:
|
221
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
222
|
-
output: int | float | None = timeout
|
223
|
-
if timeout is not None:
|
224
|
-
output = timeout / 1000
|
225
|
-
return output
|
226
|
-
|
227
|
-
|
228
|
-
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
229
|
-
"""Check arguments for `requests.Session.request`.
|
230
|
-
|
231
|
-
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
232
|
-
`requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
|
233
|
-
"""
|
234
|
-
url = data["url"]
|
235
|
-
if not urlparse(url).netloc:
|
236
|
-
stack = inspect.stack()
|
237
|
-
method_name = "call"
|
238
|
-
for frame in stack[1:]:
|
239
|
-
if frame.function == "call_and_validate":
|
240
|
-
method_name = "call_and_validate"
|
241
|
-
break
|
242
|
-
raise RuntimeError(
|
243
|
-
"The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
|
244
|
-
f"Pass `base_url` either to the `schemathesis.from_*` loader or to the `Case.{method_name}`.\n"
|
245
|
-
f"If you use the ASGI integration, please supply your test client "
|
246
|
-
f"as the `session` argument to `call`.\nURL: {url}"
|
247
|
-
)
|
248
|
-
|
249
|
-
|
250
|
-
@dataclass
|
251
|
-
class ASGITransport(RequestsTransport):
|
252
|
-
app: ASGI2App | ASGI3App
|
253
|
-
|
254
|
-
def send(
|
255
|
-
self,
|
256
|
-
case: Case,
|
257
|
-
*,
|
258
|
-
session: requests.Session | None = None,
|
259
|
-
base_url: str | None = None,
|
260
|
-
headers: dict[str, Any] | None = None,
|
261
|
-
params: dict[str, Any] | None = None,
|
262
|
-
cookies: dict[str, Any] | None = None,
|
263
|
-
**kwargs: Any,
|
264
|
-
) -> requests.Response:
|
265
|
-
from starlette_testclient import TestClient as ASGIClient
|
266
|
-
|
267
|
-
if base_url is None:
|
268
|
-
base_url = case.get_full_base_url()
|
269
|
-
with ASGIClient(self.app) as client:
|
270
|
-
return super().send(
|
271
|
-
case, session=client, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
|
272
|
-
)
|
273
|
-
|
274
|
-
|
275
|
-
@dataclass
|
276
|
-
class WSGITransport:
|
277
|
-
app: WSGIApplication
|
278
|
-
|
279
|
-
def serialize_case(
|
280
|
-
self,
|
281
|
-
case: Case,
|
282
|
-
*,
|
283
|
-
base_url: str | None = None,
|
284
|
-
headers: dict[str, Any] | None = None,
|
285
|
-
params: dict[str, Any] | None = None,
|
286
|
-
cookies: dict[str, Any] | None = None,
|
287
|
-
) -> dict[str, Any]:
|
288
|
-
final_headers = case._get_headers(headers)
|
289
|
-
media_type: str | None
|
290
|
-
if case.body is not NOT_SET and case.media_type is None:
|
291
|
-
media_type = case.operation._get_default_media_type()
|
292
|
-
else:
|
293
|
-
media_type = case.media_type
|
294
|
-
if media_type and not isinstance(case.body, NotSet):
|
295
|
-
# If we need to send a payload, then the Content-Type header should be set
|
296
|
-
final_headers["Content-Type"] = media_type
|
297
|
-
extra: dict[str, Any]
|
298
|
-
serializer = case._get_serializer(media_type)
|
299
|
-
if serializer is not None and not isinstance(case.body, NotSet):
|
300
|
-
context = SerializerContext(case=case)
|
301
|
-
extra = serializer.as_werkzeug(context, case._get_body())
|
302
|
-
else:
|
303
|
-
extra = {}
|
304
|
-
data = {
|
305
|
-
"method": case.method,
|
306
|
-
"path": case.operation.schema.get_full_path(case.formatted_path),
|
307
|
-
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
308
|
-
"headers": dict(final_headers),
|
309
|
-
"query_string": case.query,
|
310
|
-
**extra,
|
311
|
-
}
|
312
|
-
if params is not None:
|
313
|
-
_merge_dict_to(data, "query_string", params)
|
314
|
-
return data
|
315
|
-
|
316
|
-
def send(
|
317
|
-
self,
|
318
|
-
case: Case,
|
319
|
-
*,
|
320
|
-
session: Any = None,
|
321
|
-
base_url: str | None = None,
|
322
|
-
headers: dict[str, Any] | None = None,
|
323
|
-
params: dict[str, Any] | None = None,
|
324
|
-
cookies: dict[str, Any] | None = None,
|
325
|
-
**kwargs: Any,
|
326
|
-
) -> WSGIResponse:
|
327
|
-
import requests
|
328
|
-
import werkzeug
|
329
|
-
|
330
|
-
from .responses import WSGIResponse
|
331
|
-
|
332
|
-
application = kwargs.pop("app", self.app) or self.app
|
333
|
-
data = self.serialize_case(case, headers=headers, params=params)
|
334
|
-
data.update(kwargs)
|
335
|
-
client = werkzeug.Client(application, WSGIResponse)
|
336
|
-
cookies = {**(case.cookies or {}), **(cookies or {})}
|
337
|
-
with cookie_handler(client, cookies), case.operation.schema.ratelimit():
|
338
|
-
start = time.monotonic()
|
339
|
-
response = client.open(**data)
|
340
|
-
elapsed = time.monotonic() - start
|
341
|
-
requests_kwargs = RequestsTransport().serialize_case(
|
342
|
-
case,
|
343
|
-
base_url=case.get_full_base_url(),
|
344
|
-
headers=headers,
|
345
|
-
params=params,
|
346
|
-
cookies=cookies,
|
347
|
-
)
|
348
|
-
response.request = requests.Request(**requests_kwargs).prepare()
|
349
|
-
response.elapsed = timedelta(seconds=elapsed)
|
350
|
-
return response
|
351
|
-
|
352
|
-
|
353
|
-
@contextmanager
|
354
|
-
def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
|
355
|
-
"""Set cookies required for a call."""
|
356
|
-
if not cookies:
|
357
|
-
yield
|
358
|
-
else:
|
359
|
-
for key, value in cookies.items():
|
360
|
-
if IS_WERKZEUG_ABOVE_3:
|
361
|
-
client.set_cookie(key=key, value=value, domain="localhost")
|
362
|
-
else:
|
363
|
-
client.set_cookie("localhost", key=key, value=value)
|
364
|
-
yield
|
365
|
-
for key in cookies:
|
366
|
-
if IS_WERKZEUG_ABOVE_3:
|
367
|
-
client.delete_cookie(key=key, domain="localhost")
|
368
|
-
else:
|
369
|
-
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]
|
schemathesis/types.py
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
import enum
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
|
4
|
-
|
5
|
-
if TYPE_CHECKING:
|
6
|
-
from hypothesis.strategies import SearchStrategy
|
7
|
-
|
8
|
-
from .hooks import HookContext
|
9
|
-
|
10
|
-
PathLike = Union[Path, str]
|
11
|
-
|
12
|
-
Query = Dict[str, Any]
|
13
|
-
# Body can be of any Python type that corresponds to JSON Schema types + `bytes`
|
14
|
-
Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
|
15
|
-
PathParameters = Dict[str, Any]
|
16
|
-
Headers = Dict[str, Any]
|
17
|
-
Cookies = Dict[str, Any]
|
18
|
-
FormData = Dict[str, Any]
|
19
|
-
|
20
|
-
|
21
|
-
class NotSet:
|
22
|
-
pass
|
23
|
-
|
24
|
-
|
25
|
-
RequestCert = Union[str, Tuple[str, str]]
|
26
|
-
|
27
|
-
|
28
|
-
# A filter for path / method
|
29
|
-
Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
|
30
|
-
|
31
|
-
Hook = Union[
|
32
|
-
Callable[["SearchStrategy"], "SearchStrategy"], Callable[["SearchStrategy", "HookContext"], "SearchStrategy"]
|
33
|
-
]
|
34
|
-
|
35
|
-
RawAuth = Tuple[str, str]
|
36
|
-
# Generic test with any arguments and no return
|
37
|
-
GenericTest = Callable[..., None]
|
38
|
-
|
39
|
-
|
40
|
-
class Specification(str, enum.Enum):
|
41
|
-
"""Specification of the given schema."""
|
42
|
-
|
43
|
-
OPENAPI = "openapi"
|
44
|
-
GRAPHQL = "graphql"
|