schemathesis 3.26.2__py3-none-any.whl → 3.27.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +1 -1
- schemathesis/cli/__init__.py +1 -1
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/junitxml.py +79 -10
- schemathesis/cli/output/default.py +9 -47
- schemathesis/cli/reporting.py +72 -0
- schemathesis/generation/__init__.py +14 -2
- schemathesis/models.py +51 -143
- schemathesis/runner/impl/core.py +11 -12
- schemathesis/runner/serialization.py +32 -10
- schemathesis/schemas.py +11 -7
- schemathesis/specs/graphql/loaders.py +2 -0
- schemathesis/specs/graphql/schemas.py +7 -40
- schemathesis/specs/openapi/_hypothesis.py +16 -5
- schemathesis/specs/openapi/loaders.py +3 -0
- schemathesis/stateful/state_machine.py +3 -13
- schemathesis/transports/__init__.py +307 -0
- schemathesis/transports/responses.py +2 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/METADATA +2 -3
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/RECORD +23 -21
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -45,7 +45,8 @@ StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationC
|
|
|
45
45
|
def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
|
|
46
46
|
return st.text(
|
|
47
47
|
alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
|
|
48
|
-
|
|
48
|
+
# Header values with leading non-visible chars can't be sent with `requests`
|
|
49
|
+
).map(str.lstrip)
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
@lru_cache
|
|
@@ -67,8 +68,7 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
|
|
|
67
68
|
"_header_name": st.text(
|
|
68
69
|
min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
|
|
69
70
|
),
|
|
70
|
-
|
|
71
|
-
HEADER_FORMAT: header_value.map(str.lstrip),
|
|
71
|
+
HEADER_FORMAT: header_value,
|
|
72
72
|
"_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
|
|
73
73
|
"_bearer_auth": header_value.map("Bearer {}".format),
|
|
74
74
|
}
|
|
@@ -425,6 +425,15 @@ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
|
|
|
425
425
|
return value
|
|
426
426
|
|
|
427
427
|
|
|
428
|
+
def _build_custom_formats(
|
|
429
|
+
custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
|
|
430
|
+
) -> dict[str, st.SearchStrategy]:
|
|
431
|
+
custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
|
|
432
|
+
if generation_config.headers.strategy is not None:
|
|
433
|
+
custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
|
|
434
|
+
return custom_formats
|
|
435
|
+
|
|
436
|
+
|
|
428
437
|
def make_positive_strategy(
|
|
429
438
|
schema: dict[str, Any],
|
|
430
439
|
operation_name: str,
|
|
@@ -441,9 +450,10 @@ def make_positive_strategy(
|
|
|
441
450
|
for sub_schema in schema.get("properties", {}).values():
|
|
442
451
|
if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
|
|
443
452
|
sub_schema.setdefault("format", HEADER_FORMAT)
|
|
453
|
+
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
|
444
454
|
return from_schema(
|
|
445
455
|
schema,
|
|
446
|
-
custom_formats=
|
|
456
|
+
custom_formats=custom_formats,
|
|
447
457
|
allow_x00=generation_config.allow_x00,
|
|
448
458
|
codec=generation_config.codec,
|
|
449
459
|
)
|
|
@@ -462,12 +472,13 @@ def make_negative_strategy(
|
|
|
462
472
|
generation_config: GenerationConfig,
|
|
463
473
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
|
464
474
|
) -> st.SearchStrategy:
|
|
475
|
+
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
|
465
476
|
return negative_schema(
|
|
466
477
|
schema,
|
|
467
478
|
operation_name=operation_name,
|
|
468
479
|
location=location,
|
|
469
480
|
media_type=media_type,
|
|
470
|
-
custom_formats=
|
|
481
|
+
custom_formats=custom_formats,
|
|
471
482
|
generation_config=generation_config,
|
|
472
483
|
)
|
|
473
484
|
|
|
@@ -304,6 +304,7 @@ def from_dict(
|
|
|
304
304
|
:param dict raw_schema: A schema to load.
|
|
305
305
|
"""
|
|
306
306
|
from .schemas import OpenApi30, SwaggerV20
|
|
307
|
+
from ... import transports
|
|
307
308
|
|
|
308
309
|
if not isinstance(raw_schema, dict):
|
|
309
310
|
raise SchemaError(SchemaErrorType.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
|
|
@@ -338,6 +339,7 @@ def from_dict(
|
|
|
338
339
|
location=location,
|
|
339
340
|
rate_limiter=rate_limiter,
|
|
340
341
|
sanitize_output=sanitize_output,
|
|
342
|
+
transport=transports.get(app),
|
|
341
343
|
)
|
|
342
344
|
dispatch("after_load_schema", hook_context, instance)
|
|
343
345
|
return instance
|
|
@@ -379,6 +381,7 @@ def from_dict(
|
|
|
379
381
|
location=location,
|
|
380
382
|
rate_limiter=rate_limiter,
|
|
381
383
|
sanitize_output=sanitize_output,
|
|
384
|
+
transport=transports.get(app),
|
|
382
385
|
)
|
|
383
386
|
dispatch("after_load_schema", hook_context, instance)
|
|
384
387
|
return instance
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
import re
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
7
|
|
|
8
8
|
from hypothesis.errors import InvalidDefinition
|
|
9
9
|
from hypothesis.stateful import RuleBasedStateMachine
|
|
@@ -189,13 +189,12 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
189
189
|
:return: Response from the application under test.
|
|
190
190
|
|
|
191
191
|
Note that WSGI/ASGI applications are detected automatically in this method. Depending on the result of this
|
|
192
|
-
detection the state machine will call ``call
|
|
192
|
+
detection the state machine will call the ``call`` method.
|
|
193
193
|
|
|
194
194
|
Usually, you don't need to override this method unless you are building a different state machine on top of this
|
|
195
195
|
one and want to customize the transport layer itself.
|
|
196
196
|
"""
|
|
197
|
-
|
|
198
|
-
return method(**kwargs)
|
|
197
|
+
return case.call(**kwargs)
|
|
199
198
|
|
|
200
199
|
def get_call_kwargs(self, case: Case) -> dict[str, Any]:
|
|
201
200
|
"""Create custom keyword arguments that will be passed to the :meth:`Case.call` method.
|
|
@@ -214,15 +213,6 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
214
213
|
"""
|
|
215
214
|
return {}
|
|
216
215
|
|
|
217
|
-
def _get_call_method(self, case: Case) -> Callable:
|
|
218
|
-
if case.app is not None:
|
|
219
|
-
from starlette.applications import Starlette
|
|
220
|
-
|
|
221
|
-
if isinstance(case.app, Starlette):
|
|
222
|
-
return case.call_asgi
|
|
223
|
-
return case.call_wsgi
|
|
224
|
-
return case.call
|
|
225
|
-
|
|
226
216
|
def validate_response(
|
|
227
217
|
self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
|
228
218
|
) -> None:
|
|
@@ -1,5 +1,312 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
4
|
+
import time
|
|
5
|
+
from inspect import iscoroutinefunction
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from .. import failures
|
|
13
|
+
from .._dependency_versions import IS_WERKZEUG_ABOVE_3
|
|
14
|
+
from ..constants import DEFAULT_RESPONSE_TIMEOUT
|
|
15
|
+
from ..exceptions import get_timeout_error
|
|
16
|
+
from ..serializers import SerializerContext
|
|
17
|
+
from ..types import Cookies, NotSet
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import requests
|
|
21
|
+
import werkzeug
|
|
22
|
+
from _typeshed.wsgi import WSGIApplication
|
|
23
|
+
from starlette_testclient._testclient import ASGI2App, ASGI3App
|
|
24
|
+
|
|
25
|
+
from ..models import Case
|
|
26
|
+
from .responses import WSGIResponse
|
|
2
27
|
|
|
3
28
|
|
|
4
29
|
def serialize_payload(payload: bytes) -> str:
|
|
5
30
|
return base64.b64encode(payload).decode()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get(app: Any) -> Transport:
|
|
34
|
+
"""Get transport to send the data to the application."""
|
|
35
|
+
if app is None:
|
|
36
|
+
return RequestsTransport()
|
|
37
|
+
if iscoroutinefunction(app) or (
|
|
38
|
+
hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
|
|
39
|
+
):
|
|
40
|
+
return ASGITransport(app=app)
|
|
41
|
+
return WSGITransport(app=app)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
S = TypeVar("S", contravariant=True)
|
|
45
|
+
R = TypeVar("R", covariant=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Transport(Protocol[S, R]):
|
|
49
|
+
def serialize_case(
|
|
50
|
+
self,
|
|
51
|
+
case: Case,
|
|
52
|
+
*,
|
|
53
|
+
base_url: str | None = None,
|
|
54
|
+
headers: dict[str, Any] | None = None,
|
|
55
|
+
params: dict[str, Any] | None = None,
|
|
56
|
+
cookies: dict[str, Any] | None = None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def send(
|
|
61
|
+
self,
|
|
62
|
+
case: Case,
|
|
63
|
+
*,
|
|
64
|
+
session: S | None = None,
|
|
65
|
+
base_url: str | None = None,
|
|
66
|
+
headers: dict[str, Any] | None = None,
|
|
67
|
+
params: dict[str, Any] | None = None,
|
|
68
|
+
cookies: dict[str, Any] | None = None,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> R:
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RequestsTransport:
|
|
75
|
+
def serialize_case(
|
|
76
|
+
self,
|
|
77
|
+
case: Case,
|
|
78
|
+
*,
|
|
79
|
+
base_url: str | None = None,
|
|
80
|
+
headers: dict[str, Any] | None = None,
|
|
81
|
+
params: dict[str, Any] | None = None,
|
|
82
|
+
cookies: dict[str, Any] | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
final_headers = case._get_headers(headers)
|
|
85
|
+
if case.media_type and case.media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
|
|
86
|
+
# `requests` will handle multipart form headers with the proper `boundary` value.
|
|
87
|
+
if "content-type" not in final_headers:
|
|
88
|
+
final_headers["Content-Type"] = case.media_type
|
|
89
|
+
url = case._get_url(base_url)
|
|
90
|
+
serializer = case._get_serializer()
|
|
91
|
+
if serializer is not None and not isinstance(case.body, NotSet):
|
|
92
|
+
context = SerializerContext(case=case)
|
|
93
|
+
extra = serializer.as_requests(context, case._get_body())
|
|
94
|
+
else:
|
|
95
|
+
extra = {}
|
|
96
|
+
if case._auth is not None:
|
|
97
|
+
extra["auth"] = case._auth
|
|
98
|
+
additional_headers = extra.pop("headers", None)
|
|
99
|
+
if additional_headers:
|
|
100
|
+
# Additional headers, needed for the serializer
|
|
101
|
+
for key, value in additional_headers.items():
|
|
102
|
+
final_headers.setdefault(key, value)
|
|
103
|
+
data = {
|
|
104
|
+
"method": case.method,
|
|
105
|
+
"url": url,
|
|
106
|
+
"cookies": case.cookies,
|
|
107
|
+
"headers": final_headers,
|
|
108
|
+
"params": case.query,
|
|
109
|
+
**extra,
|
|
110
|
+
}
|
|
111
|
+
if params is not None:
|
|
112
|
+
_merge_dict_to(data, "params", params)
|
|
113
|
+
if cookies is not None:
|
|
114
|
+
_merge_dict_to(data, "cookies", cookies)
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
def send(
|
|
118
|
+
self,
|
|
119
|
+
case: Case,
|
|
120
|
+
*,
|
|
121
|
+
session: requests.Session | None = None,
|
|
122
|
+
base_url: str | None = None,
|
|
123
|
+
headers: dict[str, Any] | None = None,
|
|
124
|
+
params: dict[str, Any] | None = None,
|
|
125
|
+
cookies: dict[str, Any] | None = None,
|
|
126
|
+
**kwargs: Any,
|
|
127
|
+
) -> requests.Response:
|
|
128
|
+
import requests
|
|
129
|
+
from urllib3.exceptions import ReadTimeoutError
|
|
130
|
+
|
|
131
|
+
data = self.serialize_case(case, base_url=base_url, headers=headers, params=params, cookies=cookies)
|
|
132
|
+
data.update(kwargs)
|
|
133
|
+
data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
|
134
|
+
if session is None:
|
|
135
|
+
validate_vanilla_requests_kwargs(data)
|
|
136
|
+
session = requests.Session()
|
|
137
|
+
close_session = True
|
|
138
|
+
else:
|
|
139
|
+
close_session = False
|
|
140
|
+
verify = data.get("verify", True)
|
|
141
|
+
try:
|
|
142
|
+
with case.operation.schema.ratelimit():
|
|
143
|
+
response = session.request(**data) # type: ignore
|
|
144
|
+
except (requests.Timeout, requests.ConnectionError) as exc:
|
|
145
|
+
if isinstance(exc, requests.ConnectionError):
|
|
146
|
+
if not isinstance(exc.args[0], ReadTimeoutError):
|
|
147
|
+
raise
|
|
148
|
+
req = requests.Request(
|
|
149
|
+
method=data["method"].upper(),
|
|
150
|
+
url=data["url"],
|
|
151
|
+
headers=data["headers"],
|
|
152
|
+
files=data.get("files"),
|
|
153
|
+
data=data.get("data") or {},
|
|
154
|
+
json=data.get("json"),
|
|
155
|
+
params=data.get("params") or {},
|
|
156
|
+
auth=data.get("auth"),
|
|
157
|
+
cookies=data["cookies"],
|
|
158
|
+
hooks=data.get("hooks"),
|
|
159
|
+
)
|
|
160
|
+
request = session.prepare_request(req)
|
|
161
|
+
else:
|
|
162
|
+
request = cast(requests.PreparedRequest, exc.request)
|
|
163
|
+
timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
|
|
164
|
+
code_message = case._get_code_message(case.operation.schema.code_sample_style, request, verify=verify)
|
|
165
|
+
message = f"The server failed to respond within the specified limit of {timeout:.2f}ms"
|
|
166
|
+
raise get_timeout_error(timeout)(
|
|
167
|
+
f"\n\n1. {failures.RequestTimeout.title}\n\n{message}\n\n{code_message}",
|
|
168
|
+
context=failures.RequestTimeout(message=message, timeout=timeout),
|
|
169
|
+
) from None
|
|
170
|
+
response.verify = verify # type: ignore[attr-defined]
|
|
171
|
+
if close_session:
|
|
172
|
+
session.close()
|
|
173
|
+
return response
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
|
|
177
|
+
original = data[data_key] or {}
|
|
178
|
+
for key, value in new.items():
|
|
179
|
+
original[key] = value
|
|
180
|
+
data[data_key] = original
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
|
184
|
+
"""Check arguments for `requests.Session.request`.
|
|
185
|
+
|
|
186
|
+
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
|
187
|
+
`requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
|
|
188
|
+
"""
|
|
189
|
+
url = data["url"]
|
|
190
|
+
if not urlparse(url).netloc:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
"The URL should be absolute, so Schemathesis knows where to send the data. \n"
|
|
193
|
+
f"If you use the ASGI integration, please supply your test client "
|
|
194
|
+
f"as the `session` argument to `call`.\nURL: {url}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class ASGITransport(RequestsTransport):
|
|
200
|
+
app: ASGI2App | ASGI3App
|
|
201
|
+
|
|
202
|
+
def send(
|
|
203
|
+
self,
|
|
204
|
+
case: Case,
|
|
205
|
+
*,
|
|
206
|
+
session: requests.Session | None = None,
|
|
207
|
+
base_url: str | None = None,
|
|
208
|
+
headers: dict[str, Any] | None = None,
|
|
209
|
+
params: dict[str, Any] | None = None,
|
|
210
|
+
cookies: dict[str, Any] | None = None,
|
|
211
|
+
**kwargs: Any,
|
|
212
|
+
) -> requests.Response:
|
|
213
|
+
from starlette_testclient import TestClient as ASGIClient
|
|
214
|
+
|
|
215
|
+
if base_url is None:
|
|
216
|
+
base_url = case.get_full_base_url()
|
|
217
|
+
with ASGIClient(self.app) as client:
|
|
218
|
+
return super().send(
|
|
219
|
+
case, session=client, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass
|
|
224
|
+
class WSGITransport:
|
|
225
|
+
app: WSGIApplication
|
|
226
|
+
|
|
227
|
+
def serialize_case(
|
|
228
|
+
self,
|
|
229
|
+
case: Case,
|
|
230
|
+
*,
|
|
231
|
+
base_url: str | None = None,
|
|
232
|
+
headers: dict[str, Any] | None = None,
|
|
233
|
+
params: dict[str, Any] | None = None,
|
|
234
|
+
cookies: dict[str, Any] | None = None,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
final_headers = case._get_headers(headers)
|
|
237
|
+
if case.media_type and not isinstance(case.body, NotSet):
|
|
238
|
+
# If we need to send a payload, then the Content-Type header should be set
|
|
239
|
+
final_headers["Content-Type"] = case.media_type
|
|
240
|
+
extra: dict[str, Any]
|
|
241
|
+
serializer = case._get_serializer()
|
|
242
|
+
if serializer is not None and not isinstance(case.body, NotSet):
|
|
243
|
+
context = SerializerContext(case=case)
|
|
244
|
+
extra = serializer.as_werkzeug(context, case._get_body())
|
|
245
|
+
else:
|
|
246
|
+
extra = {}
|
|
247
|
+
data = {
|
|
248
|
+
"method": case.method,
|
|
249
|
+
"path": case.operation.schema.get_full_path(case.formatted_path),
|
|
250
|
+
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
|
251
|
+
"headers": dict(final_headers),
|
|
252
|
+
"query_string": case.query,
|
|
253
|
+
**extra,
|
|
254
|
+
}
|
|
255
|
+
if params is not None:
|
|
256
|
+
_merge_dict_to(data, "query_string", params)
|
|
257
|
+
return data
|
|
258
|
+
|
|
259
|
+
def send(
|
|
260
|
+
self,
|
|
261
|
+
case: Case,
|
|
262
|
+
*,
|
|
263
|
+
session: Any = None,
|
|
264
|
+
base_url: str | None = None,
|
|
265
|
+
headers: dict[str, Any] | None = None,
|
|
266
|
+
params: dict[str, Any] | None = None,
|
|
267
|
+
cookies: dict[str, Any] | None = None,
|
|
268
|
+
**kwargs: Any,
|
|
269
|
+
) -> WSGIResponse:
|
|
270
|
+
import requests
|
|
271
|
+
import werkzeug
|
|
272
|
+
|
|
273
|
+
from .responses import WSGIResponse
|
|
274
|
+
|
|
275
|
+
application = kwargs.pop("app", self.app) or self.app
|
|
276
|
+
data = self.serialize_case(case, headers=headers, params=params)
|
|
277
|
+
data.update(kwargs)
|
|
278
|
+
client = werkzeug.Client(application, WSGIResponse)
|
|
279
|
+
cookies = {**(case.cookies or {}), **(cookies or {})}
|
|
280
|
+
with cookie_handler(client, cookies), case.operation.schema.ratelimit():
|
|
281
|
+
start = time.monotonic()
|
|
282
|
+
response = client.open(**data)
|
|
283
|
+
elapsed = time.monotonic() - start
|
|
284
|
+
requests_kwargs = RequestsTransport().serialize_case(
|
|
285
|
+
case,
|
|
286
|
+
base_url=case.get_full_base_url(),
|
|
287
|
+
headers=headers,
|
|
288
|
+
params=params,
|
|
289
|
+
cookies=cookies,
|
|
290
|
+
)
|
|
291
|
+
response.request = requests.Request(**requests_kwargs).prepare()
|
|
292
|
+
response.elapsed = timedelta(seconds=elapsed)
|
|
293
|
+
return response
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@contextmanager
|
|
297
|
+
def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
|
|
298
|
+
"""Set cookies required for a call."""
|
|
299
|
+
if not cookies:
|
|
300
|
+
yield
|
|
301
|
+
else:
|
|
302
|
+
for key, value in cookies.items():
|
|
303
|
+
if IS_WERKZEUG_ABOVE_3:
|
|
304
|
+
client.set_cookie(key=key, value=value, domain="localhost")
|
|
305
|
+
else:
|
|
306
|
+
client.set_cookie("localhost", key=key, value=value)
|
|
307
|
+
yield
|
|
308
|
+
for key in cookies:
|
|
309
|
+
if IS_WERKZEUG_ABOVE_3:
|
|
310
|
+
client.delete_cookie(key=key, domain="localhost")
|
|
311
|
+
else:
|
|
312
|
+
client.delete_cookie("localhost", key=key)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
import json
|
|
5
|
+
from datetime import timedelta
|
|
5
6
|
from typing import Union, TYPE_CHECKING, NoReturn, Any
|
|
6
7
|
from .._compat import JSONMixin
|
|
7
8
|
from werkzeug.wrappers import Response as BaseResponse
|
|
@@ -15,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
15
16
|
class WSGIResponse(BaseResponse, JSONMixin):
|
|
16
17
|
# We store "requests" request to build a reproduction code
|
|
17
18
|
request: PreparedRequest
|
|
19
|
+
elapsed: timedelta
|
|
18
20
|
|
|
19
21
|
def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
|
|
20
22
|
# We don't need a werkzeug-specific exception when JSON parsing error happens
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.27.1
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -28,7 +28,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
28
28
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
29
29
|
Classifier: Topic :: Software Development :: Testing
|
|
30
30
|
Requires-Python: >=3.8
|
|
31
|
-
Requires-Dist: anyio<4
|
|
32
31
|
Requires-Dist: backoff<3.0,>=2.1.2
|
|
33
32
|
Requires-Dist: click<9.0,>=7.0
|
|
34
33
|
Requires-Dist: colorama<1.0,>=0.4
|
|
@@ -44,7 +43,7 @@ Requires-Dist: pytest-subtests<0.8.0,>=0.2.1
|
|
|
44
43
|
Requires-Dist: pytest<9,>=4.6.4
|
|
45
44
|
Requires-Dist: pyyaml<7.0,>=5.1
|
|
46
45
|
Requires-Dist: requests<3,>=2.22
|
|
47
|
-
Requires-Dist: starlette-testclient<1
|
|
46
|
+
Requires-Dist: starlette-testclient<1,>=0.4.1
|
|
48
47
|
Requires-Dist: starlette<1,>=0.13
|
|
49
48
|
Requires-Dist: tomli-w<2.0,>=1.0.0
|
|
50
49
|
Requires-Dist: tomli<3.0,>=2.0.1
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
schemathesis/__init__.py,sha256=
|
|
1
|
+
schemathesis/__init__.py,sha256=zAa981OnVdIdjiETPP5a5vZL8QXAh2HbTWj8iEoxKC4,1984
|
|
2
2
|
schemathesis/_compat.py,sha256=VizWR0QAVj4l7WZautNe_zmo3AJ7WBl2zrJQOfJAQtk,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=InIv6MZmuRVHc_9FxAiRg7_dY-vuF0jT69FBxrBLK6U,879
|
|
4
4
|
schemathesis/_hypothesis.py,sha256=O3rfMbT0rChFONMUsYmMEGV9nPG5cHd_6v9NhWDI_IQ,10763
|
|
@@ -17,28 +17,30 @@ schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
|
17
17
|
schemathesis/hooks.py,sha256=cNJgCh7SyLWT1sYDKF5ncDC80ld08CinvKo2IqLMV4g,12396
|
|
18
18
|
schemathesis/lazy.py,sha256=CivWpvesh4iYLSkatXbQPTEOruWmXvuZQ29gng5p9wM,14846
|
|
19
19
|
schemathesis/loaders.py,sha256=RJnrbf-3vZ7KXmRBkmr3uqWyg0eHzOnONABuudWcTIg,4586
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=KtWx7k9M8wrkaCZ82kfLMKd7jmvFxZnGyUuTE_nPhU0,42974
|
|
21
21
|
schemathesis/parameters.py,sha256=VheEffVzoSfYaSEcG7KhPlA4ypifosG8biiHifzwL8g,2257
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=WSV_MB5YRrYkp1pQRPHrzsidqsKcqYZiq64N9grKobo,8956
|
|
24
|
-
schemathesis/schemas.py,sha256=
|
|
24
|
+
schemathesis/schemas.py,sha256=RzxAoBoJJSLsixrkTZPe2N_oWbWv8n5RL4XaYRyoSIo,18531
|
|
25
25
|
schemathesis/serializers.py,sha256=_xoebWkVrgbGbPjPgTgwuN-fN4YT004aj7kImyPComY,11619
|
|
26
26
|
schemathesis/targets.py,sha256=tzp7VZ2-7g2nZHCooRgFzTMtOVcbl0rvtNR421hQthA,1162
|
|
27
27
|
schemathesis/throttling.py,sha256=uwhL4XWPWAU8HECg0NhibfCMn5dT7NElTx3fdL3Mmcc,1065
|
|
28
28
|
schemathesis/types.py,sha256=AglR5M0bce-YXeDRkweToXTP0GjNOWVjS_mIsxLobwc,919
|
|
29
29
|
schemathesis/utils.py,sha256=4HXvHysnHp-Uz2HfNgLfW5F5VjL-mtixrjjzRCEJhYo,5233
|
|
30
|
-
schemathesis/cli/__init__.py,sha256=
|
|
30
|
+
schemathesis/cli/__init__.py,sha256=oTBqweV5CP681LqRFdRXtkd0VyYoG_xUKshkKEPnqIQ,64871
|
|
31
|
+
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
31
32
|
schemathesis/cli/callbacks.py,sha256=HmD0WmSYf5x8v4xeZdOKzy_8CEk23NitUlL8JQ7LHdQ,14950
|
|
32
33
|
schemathesis/cli/cassettes.py,sha256=1ucgYyOZiNu44uOv044FdfKTzQolUyhAM2onk4m3MD0,12988
|
|
33
34
|
schemathesis/cli/constants.py,sha256=XoezT0_fHuiOY2e--cmBUhNieJsepcUoW8e48QuLSDI,1544
|
|
34
35
|
schemathesis/cli/context.py,sha256=wwfdSL_NKHEyzQnj8PcnaJdzasa_N08e2KPvy4Wl8N4,1874
|
|
35
36
|
schemathesis/cli/debug.py,sha256=PDEa-oHyz5bZ8aYjRYARwQaCv_AC6HM_L43LJfe4vUM,657
|
|
36
37
|
schemathesis/cli/handlers.py,sha256=62GPWPmgeGUz3pkDd01H4UCXcIy5a9yRtU7qaAeeR-A,389
|
|
37
|
-
schemathesis/cli/junitxml.py,sha256=
|
|
38
|
+
schemathesis/cli/junitxml.py,sha256=JMMtWz7grPQw66Ruh-tLR8YVn-58Q0o0l_MxZWQ4V3M,4586
|
|
38
39
|
schemathesis/cli/options.py,sha256=7_dXcrPT0kWqAkm60cAT1J0IsTOcvNFxT1pcHYttBuI,2558
|
|
40
|
+
schemathesis/cli/reporting.py,sha256=uTtoEJT4maJRGPsVZH184S0hohX-4SwO_dAL__i6H10,3401
|
|
39
41
|
schemathesis/cli/sanitization.py,sha256=v-9rsMCBpWhom0Bfzj_8c6InqxkVjSWK6kRoURa52Nk,727
|
|
40
42
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
|
41
|
-
schemathesis/cli/output/default.py,sha256=
|
|
43
|
+
schemathesis/cli/output/default.py,sha256=GjT-j9pNxVYQmU6nOu08hBqvR9uD_TMI15jZitWgI0A,36781
|
|
42
44
|
schemathesis/cli/output/short.py,sha256=MmFMOKBRqqvgq7Kmx6XJc7Vqkr9g5BrJBxRbyZ0Rh-k,2068
|
|
43
45
|
schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
|
|
44
46
|
schemathesis/contrib/unique_data.py,sha256=zxrmAlQH7Bcil9YSfy2EazwLj2rxLzOvAE3O6QRRkFY,1317
|
|
@@ -55,7 +57,7 @@ schemathesis/extra/pytest_plugin.py,sha256=44RRkjkN1JupWu0muq9B5b3K9goucYndB394Q
|
|
|
55
57
|
schemathesis/fixups/__init__.py,sha256=iTpOm-cwyZQRglbWRJHxj3k7WO9a4a-MCq-UDQa6U0Y,966
|
|
56
58
|
schemathesis/fixups/fast_api.py,sha256=-gjYNSUtiKl7cKgyQ_rON0Oo_sGm6r7rOQDH0ahG32A,1322
|
|
57
59
|
schemathesis/fixups/utf8_bom.py,sha256=ttzPOaJb6K7QtNAbIZ_TWrrm0N9FylS8QzO6zI0jXgY,764
|
|
58
|
-
schemathesis/generation/__init__.py,sha256=
|
|
60
|
+
schemathesis/generation/__init__.py,sha256=UXRrDTaUEX29VEK9bLqVi_xVzyNNqul3WhpLh1za90M,2277
|
|
59
61
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
60
62
|
schemathesis/internal/copy.py,sha256=xsC7RyrFa9KadMsj9ai_aAy0XZg0oplz3cFZCka_2fg,459
|
|
61
63
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
@@ -67,9 +69,9 @@ schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QA
|
|
|
67
69
|
schemathesis/runner/__init__.py,sha256=kkqjC_5G2Mrq00syLNUDHP3sXqwiId8_cusKtIlOyXM,21419
|
|
68
70
|
schemathesis/runner/events.py,sha256=eZc4TT8C8mWCMr9h4JvlijvQQ-x8wFry8LntdsYIL-A,10200
|
|
69
71
|
schemathesis/runner/probes.py,sha256=t_B38-ljy-p3Odw-dqcZUVIjYTPwBvac8KE5GaLnz4M,5546
|
|
70
|
-
schemathesis/runner/serialization.py,sha256=
|
|
72
|
+
schemathesis/runner/serialization.py,sha256=7DBve7E6JAbAHH5npzjME7lFnysKCc9dPXMmdIIYrNI,16667
|
|
71
73
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
72
|
-
schemathesis/runner/impl/core.py,sha256=
|
|
74
|
+
schemathesis/runner/impl/core.py,sha256=Y8WyKEUeuEs0xi9hWmEA63a39-NO7L6I5hQRhwo9JBQ,39621
|
|
73
75
|
schemathesis/runner/impl/solo.py,sha256=Y_tNhVBVxcuxv65hN0FjxLlVSC41ebHMOM1xSzVrNk8,3358
|
|
74
76
|
schemathesis/runner/impl/threadpool.py,sha256=2-2Wvw7msezZtenZY5vU_x3FGLLVlH-ywvhU9hTwAAo,15073
|
|
75
77
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
|
@@ -87,13 +89,13 @@ schemathesis/service/serialization.py,sha256=OjWG7FfihEl4LtpOD8N-fmjOTzSPUm6A4x6
|
|
|
87
89
|
schemathesis/service/usage.py,sha256=Z-GCwFcW1pS6YdC-ziEOynikqgOttxp2Uyj_dfK5Q7A,2437
|
|
88
90
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
91
|
schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
|
|
90
|
-
schemathesis/specs/graphql/loaders.py,sha256=
|
|
92
|
+
schemathesis/specs/graphql/loaders.py,sha256=5BcHD-w0XU_CRfS53SN4iuGDOfvUfjOfyiRMRGmNDVc,11461
|
|
91
93
|
schemathesis/specs/graphql/nodes.py,sha256=7F5jbk96uTZZXK9Ulr86KpCAn8z6LKMBcrLrdJHggH0,540
|
|
92
94
|
schemathesis/specs/graphql/scalars.py,sha256=W5oj6AcjiXpR-Z6eSSp1oPWl0mjH2NF-w87nRFhrHHg,1805
|
|
93
|
-
schemathesis/specs/graphql/schemas.py,sha256=
|
|
95
|
+
schemathesis/specs/graphql/schemas.py,sha256=6BlTF9iAongsDxzy4_l85YjDob2QzpB0Vt7mlbIUxaw,12637
|
|
94
96
|
schemathesis/specs/graphql/validation.py,sha256=SqQbj9uymGUQxlHXc8HkQccIq25uwP5CvLF1zReb1Xg,1636
|
|
95
97
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
96
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
98
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=Qrm6Po2W2wsxZAlLFRUwq0svO8SmQp-5InCFa2cwYaw,23275
|
|
97
99
|
schemathesis/specs/openapi/checks.py,sha256=1WB_UGNqptfJThWLUNbds1Q-IzOGbbHCOYPIhBYk-zs,5411
|
|
98
100
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
99
101
|
schemathesis/specs/openapi/converter.py,sha256=9TKeKvNi9MVvoNMfqoPz_cODO8oNrMSTXTOwLLfjD_Q,2799
|
|
@@ -102,7 +104,7 @@ schemathesis/specs/openapi/examples.py,sha256=igLDfMpNhRMAk7mYBqi93CtVRQTZCWjCJ2
|
|
|
102
104
|
schemathesis/specs/openapi/filters.py,sha256=Ei-QTFcGCvGSIunT-GYQrtqzB-kqvUePOcUuC7B7mT8,1436
|
|
103
105
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
104
106
|
schemathesis/specs/openapi/links.py,sha256=FUjqEf6Sv6PeS0UX7CLL5p2VHa9vA1MGDPhx2pfYg1s,14576
|
|
105
|
-
schemathesis/specs/openapi/loaders.py,sha256=
|
|
107
|
+
schemathesis/specs/openapi/loaders.py,sha256=QoEFJ7fxJz5XcGTEWV_ZGXARiektmyyT_xmVtcHco6Q,24131
|
|
106
108
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
107
109
|
schemathesis/specs/openapi/parameters.py,sha256=5jMZQZFsZNBFjG22Bqot-Rc65emiSA4E95rIzwImThk,13610
|
|
108
110
|
schemathesis/specs/openapi/references.py,sha256=YGunHAGubYPKbMqQtpFWZm1D4AGxB8wLuiVhE6T6cWo,8978
|
|
@@ -124,14 +126,14 @@ schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrr
|
|
|
124
126
|
schemathesis/specs/openapi/stateful/__init__.py,sha256=R8RhPJD3rDzqL4eA9jSnUwh9Q_Mv27ka1C5FdRuyusY,4509
|
|
125
127
|
schemathesis/specs/openapi/stateful/links.py,sha256=cSIwho2Hroty6au-gyCD-OwqnuCcIpnIIHU6FvF0SwA,3533
|
|
126
128
|
schemathesis/stateful/__init__.py,sha256=T7rvhzftfl3wumEsir33DBBzCTK2PtRp9CxBxMLdMSE,4693
|
|
127
|
-
schemathesis/stateful/state_machine.py,sha256=
|
|
128
|
-
schemathesis/transports/__init__.py,sha256=
|
|
129
|
+
schemathesis/stateful/state_machine.py,sha256=ZcKpbvEl1QGhVOYnA7Ow6zkuFHtEPDAyCjroPrj-FgU,11343
|
|
130
|
+
schemathesis/transports/__init__.py,sha256=IHQaob4pLHPP0zXXJukWEQBpV4aSIhGPHHMm0up8x64,11287
|
|
129
131
|
schemathesis/transports/auth.py,sha256=ZKFku9gjhIG6445qNC2p_64Yt9Iz_4azbvja8AMptBk,404
|
|
130
132
|
schemathesis/transports/content_types.py,sha256=xU8RZWxz-CyWRqQTI2fGYQacB7KasoY7LL_bxPQdyPY,2144
|
|
131
133
|
schemathesis/transports/headers.py,sha256=EDxpm8su0AuhyqZUkMex-OFZMAJN_5NHah7fDT2HDZE,989
|
|
132
|
-
schemathesis/transports/responses.py,sha256=
|
|
133
|
-
schemathesis-3.
|
|
134
|
-
schemathesis-3.
|
|
135
|
-
schemathesis-3.
|
|
136
|
-
schemathesis-3.
|
|
137
|
-
schemathesis-3.
|
|
134
|
+
schemathesis/transports/responses.py,sha256=j_-udvWbmi6XtEYmpdX8WoFnlrQ6-i3PSBizfozRjQI,1673
|
|
135
|
+
schemathesis-3.27.1.dist-info/METADATA,sha256=GtcBreI2a4R9b3KL_eS-QQb57LTmQckmsQ_vkPTIdDs,16242
|
|
136
|
+
schemathesis-3.27.1.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
137
|
+
schemathesis-3.27.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
138
|
+
schemathesis-3.27.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
139
|
+
schemathesis-3.27.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|