schemathesis 3.21.2__py3-none-any.whl → 3.22.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/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/models.py
CHANGED
|
@@ -13,18 +13,14 @@ from typing import (
|
|
|
13
13
|
TYPE_CHECKING,
|
|
14
14
|
Any,
|
|
15
15
|
Callable,
|
|
16
|
-
Dict,
|
|
17
16
|
Generator,
|
|
18
17
|
Generic,
|
|
19
18
|
Iterator,
|
|
20
|
-
List,
|
|
21
19
|
NoReturn,
|
|
22
20
|
Optional,
|
|
23
21
|
Sequence,
|
|
24
|
-
Tuple,
|
|
25
22
|
Type,
|
|
26
23
|
TypeVar,
|
|
27
|
-
Union,
|
|
28
24
|
cast,
|
|
29
25
|
)
|
|
30
26
|
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
@@ -33,7 +29,7 @@ from . import failures, serializers
|
|
|
33
29
|
from ._dependency_versions import IS_WERKZEUG_ABOVE_3
|
|
34
30
|
from .auths import AuthStorage
|
|
35
31
|
from .code_samples import CodeSampleStyle
|
|
36
|
-
from .generation import DataGenerationMethod
|
|
32
|
+
from .generation import DataGenerationMethod, GenerationConfig
|
|
37
33
|
from .constants import (
|
|
38
34
|
DEFAULT_RESPONSE_TIMEOUT,
|
|
39
35
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
@@ -51,6 +47,7 @@ from .exceptions import (
|
|
|
51
47
|
get_grouped_exception,
|
|
52
48
|
get_timeout_error,
|
|
53
49
|
prepare_response_payload,
|
|
50
|
+
SkipTest,
|
|
54
51
|
)
|
|
55
52
|
from .internal.deprecation import deprecated_property
|
|
56
53
|
from .internal.copy import fast_deepcopy
|
|
@@ -64,6 +61,7 @@ from .generation import generate_random_case_id
|
|
|
64
61
|
|
|
65
62
|
if TYPE_CHECKING:
|
|
66
63
|
import werkzeug
|
|
64
|
+
import unittest
|
|
67
65
|
from requests.structures import CaseInsensitiveDict
|
|
68
66
|
from hypothesis import strategies as st
|
|
69
67
|
import requests.auth
|
|
@@ -76,11 +74,11 @@ if TYPE_CHECKING:
|
|
|
76
74
|
class CaseSource:
|
|
77
75
|
"""Data sources, used to generate a test case."""
|
|
78
76
|
|
|
79
|
-
case:
|
|
77
|
+
case: Case
|
|
80
78
|
response: GenericResponse
|
|
81
79
|
elapsed: float
|
|
82
80
|
|
|
83
|
-
def partial_deepcopy(self) ->
|
|
81
|
+
def partial_deepcopy(self) -> CaseSource:
|
|
84
82
|
from .transports.responses import copy_response
|
|
85
83
|
|
|
86
84
|
return self.__class__(
|
|
@@ -98,7 +96,7 @@ def cant_serialize(media_type: str) -> NoReturn: # type: ignore
|
|
|
98
96
|
reject() # type: ignore
|
|
99
97
|
|
|
100
98
|
|
|
101
|
-
@lru_cache
|
|
99
|
+
@lru_cache
|
|
102
100
|
def get_request_signature() -> inspect.Signature:
|
|
103
101
|
import requests
|
|
104
102
|
|
|
@@ -109,11 +107,11 @@ def get_request_signature() -> inspect.Signature:
|
|
|
109
107
|
class PreparedRequestData:
|
|
110
108
|
method: str
|
|
111
109
|
url: str
|
|
112
|
-
body:
|
|
110
|
+
body: str | bytes | None
|
|
113
111
|
headers: Headers
|
|
114
112
|
|
|
115
113
|
|
|
116
|
-
def prepare_request_data(kwargs:
|
|
114
|
+
def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
|
|
117
115
|
"""Prepare request data for generating code samples."""
|
|
118
116
|
import requests
|
|
119
117
|
|
|
@@ -128,23 +126,23 @@ def prepare_request_data(kwargs: Dict[str, Any]) -> PreparedRequestData:
|
|
|
128
126
|
class Case:
|
|
129
127
|
"""A single test case parameters."""
|
|
130
128
|
|
|
131
|
-
operation:
|
|
129
|
+
operation: APIOperation
|
|
132
130
|
# Unique test case identifier
|
|
133
131
|
id: str = field(default_factory=generate_random_case_id, compare=False)
|
|
134
|
-
path_parameters:
|
|
135
|
-
headers:
|
|
136
|
-
cookies:
|
|
137
|
-
query:
|
|
132
|
+
path_parameters: PathParameters | None = None
|
|
133
|
+
headers: CaseInsensitiveDict | None = None
|
|
134
|
+
cookies: Cookies | None = None
|
|
135
|
+
query: Query | None = None
|
|
138
136
|
# By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
|
|
139
137
|
# which is a valid payload.
|
|
140
|
-
body:
|
|
138
|
+
body: Body | NotSet = NOT_SET
|
|
141
139
|
# The media type for cases with a payload. For example, "application/json"
|
|
142
|
-
media_type:
|
|
143
|
-
source:
|
|
140
|
+
media_type: str | None = None
|
|
141
|
+
source: CaseSource | None = None
|
|
144
142
|
|
|
145
143
|
# The way the case was generated (None for manually crafted ones)
|
|
146
|
-
data_generation_method:
|
|
147
|
-
_auth:
|
|
144
|
+
data_generation_method: DataGenerationMethod | None = None
|
|
145
|
+
_auth: requests.auth.AuthBase | None = None
|
|
148
146
|
|
|
149
147
|
def __repr__(self) -> str:
|
|
150
148
|
parts = [f"{self.__class__.__name__}("]
|
|
@@ -163,7 +161,7 @@ class Case:
|
|
|
163
161
|
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
|
164
162
|
|
|
165
163
|
@deprecated_property(removed_in="4.0", replacement="operation")
|
|
166
|
-
def endpoint(self) ->
|
|
164
|
+
def endpoint(self) -> APIOperation:
|
|
167
165
|
return self.operation
|
|
168
166
|
|
|
169
167
|
@property
|
|
@@ -179,14 +177,14 @@ class Case:
|
|
|
179
177
|
return self.operation.method.upper()
|
|
180
178
|
|
|
181
179
|
@property
|
|
182
|
-
def base_url(self) ->
|
|
180
|
+
def base_url(self) -> str | None:
|
|
183
181
|
return self.operation.base_url
|
|
184
182
|
|
|
185
183
|
@property
|
|
186
184
|
def app(self) -> Any:
|
|
187
185
|
return self.operation.app
|
|
188
186
|
|
|
189
|
-
def set_source(self, response: GenericResponse, case:
|
|
187
|
+
def set_source(self, response: GenericResponse, case: Case, elapsed: float) -> None:
|
|
190
188
|
self.source = CaseSource(case=case, response=response, elapsed=elapsed)
|
|
191
189
|
|
|
192
190
|
@property
|
|
@@ -202,7 +200,7 @@ class Case:
|
|
|
202
200
|
# A single unmatched `}` inside the path template may cause this
|
|
203
201
|
raise OperationSchemaError(f"Malformed path template: `{self.path}`\n\n {exc}") from exc
|
|
204
202
|
|
|
205
|
-
def get_full_base_url(self) ->
|
|
203
|
+
def get_full_base_url(self) -> str | None:
|
|
206
204
|
"""Create a full base url, adding "localhost" for WSGI apps."""
|
|
207
205
|
parts = urlsplit(self.base_url)
|
|
208
206
|
if not parts.hostname:
|
|
@@ -210,15 +208,15 @@ class Case:
|
|
|
210
208
|
return urlunsplit(("http", "localhost", path or "", "", ""))
|
|
211
209
|
return self.base_url
|
|
212
210
|
|
|
213
|
-
def prepare_code_sample_data(self, headers:
|
|
211
|
+
def prepare_code_sample_data(self, headers: dict[str, Any] | None) -> PreparedRequestData:
|
|
214
212
|
base_url = self.get_full_base_url()
|
|
215
213
|
kwargs = self.as_requests_kwargs(base_url, headers=headers)
|
|
216
214
|
return prepare_request_data(kwargs)
|
|
217
215
|
|
|
218
216
|
def get_code_to_reproduce(
|
|
219
217
|
self,
|
|
220
|
-
headers:
|
|
221
|
-
request:
|
|
218
|
+
headers: dict[str, Any] | None = None,
|
|
219
|
+
request: requests.PreparedRequest | None = None,
|
|
222
220
|
verify: bool = True,
|
|
223
221
|
) -> str:
|
|
224
222
|
"""Construct a Python code to reproduce this case with `requests`."""
|
|
@@ -242,7 +240,7 @@ class Case:
|
|
|
242
240
|
extra_headers=request_data.headers,
|
|
243
241
|
)
|
|
244
242
|
|
|
245
|
-
def as_curl_command(self, headers:
|
|
243
|
+
def as_curl_command(self, headers: dict[str, Any] | None = None, verify: bool = True) -> str:
|
|
246
244
|
"""Construct a curl command for a given case."""
|
|
247
245
|
request_data = self.prepare_code_sample_data(headers)
|
|
248
246
|
return CodeSampleStyle.curl.generate(
|
|
@@ -254,7 +252,7 @@ class Case:
|
|
|
254
252
|
extra_headers=request_data.headers,
|
|
255
253
|
)
|
|
256
254
|
|
|
257
|
-
def _get_base_url(self, base_url:
|
|
255
|
+
def _get_base_url(self, base_url: str | None = None) -> str:
|
|
258
256
|
if base_url is None:
|
|
259
257
|
if self.base_url is not None:
|
|
260
258
|
base_url = self.base_url
|
|
@@ -265,7 +263,7 @@ class Case:
|
|
|
265
263
|
)
|
|
266
264
|
return base_url
|
|
267
265
|
|
|
268
|
-
def _get_headers(self, headers:
|
|
266
|
+
def _get_headers(self, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
|
|
269
267
|
from requests.structures import CaseInsensitiveDict
|
|
270
268
|
|
|
271
269
|
final_headers = self.headers.copy() if self.headers is not None else CaseInsensitiveDict()
|
|
@@ -275,7 +273,7 @@ class Case:
|
|
|
275
273
|
final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
|
|
276
274
|
return final_headers
|
|
277
275
|
|
|
278
|
-
def _get_serializer(self) ->
|
|
276
|
+
def _get_serializer(self) -> Serializer | None:
|
|
279
277
|
"""Get a serializer for the payload, if there is any."""
|
|
280
278
|
if self.media_type is not None:
|
|
281
279
|
media_type = serializers.get_first_matching_media_type(self.media_type)
|
|
@@ -288,9 +286,7 @@ class Case:
|
|
|
288
286
|
return cls()
|
|
289
287
|
return None
|
|
290
288
|
|
|
291
|
-
def as_requests_kwargs(
|
|
292
|
-
self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
|
|
293
|
-
) -> Dict[str, Any]:
|
|
289
|
+
def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
|
294
290
|
"""Convert the case into a dictionary acceptable by requests."""
|
|
295
291
|
final_headers = self._get_headers(headers)
|
|
296
292
|
if self.media_type and self.media_type != "multipart/form-data" and not isinstance(self.body, NotSet):
|
|
@@ -302,7 +298,7 @@ class Case:
|
|
|
302
298
|
if not base_url.endswith("/"):
|
|
303
299
|
base_url += "/"
|
|
304
300
|
url = unquote(urljoin(base_url, quote(formatted_path)))
|
|
305
|
-
extra:
|
|
301
|
+
extra: dict[str, Any]
|
|
306
302
|
serializer = self._get_serializer()
|
|
307
303
|
if serializer is not None and not isinstance(self.body, NotSet):
|
|
308
304
|
context = SerializerContext(case=self)
|
|
@@ -327,11 +323,11 @@ class Case:
|
|
|
327
323
|
|
|
328
324
|
def call(
|
|
329
325
|
self,
|
|
330
|
-
base_url:
|
|
331
|
-
session:
|
|
332
|
-
headers:
|
|
333
|
-
params:
|
|
334
|
-
cookies:
|
|
326
|
+
base_url: str | None = None,
|
|
327
|
+
session: requests.Session | None = None,
|
|
328
|
+
headers: dict[str, Any] | None = None,
|
|
329
|
+
params: dict[str, Any] | None = None,
|
|
330
|
+
cookies: dict[str, Any] | None = None,
|
|
335
331
|
**kwargs: Any,
|
|
336
332
|
) -> requests.Response:
|
|
337
333
|
import requests
|
|
@@ -371,13 +367,13 @@ class Case:
|
|
|
371
367
|
session.close()
|
|
372
368
|
return response
|
|
373
369
|
|
|
374
|
-
def as_werkzeug_kwargs(self, headers:
|
|
370
|
+
def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
|
375
371
|
"""Convert the case into a dictionary acceptable by werkzeug.Client."""
|
|
376
372
|
final_headers = self._get_headers(headers)
|
|
377
373
|
if self.media_type and not isinstance(self.body, NotSet):
|
|
378
374
|
# If we need to send a payload, then the Content-Type header should be set
|
|
379
375
|
final_headers["Content-Type"] = self.media_type
|
|
380
|
-
extra:
|
|
376
|
+
extra: dict[str, Any]
|
|
381
377
|
serializer = self._get_serializer()
|
|
382
378
|
if serializer is not None and not isinstance(self.body, NotSet):
|
|
383
379
|
context = SerializerContext(case=self)
|
|
@@ -396,8 +392,8 @@ class Case:
|
|
|
396
392
|
def call_wsgi(
|
|
397
393
|
self,
|
|
398
394
|
app: Any = None,
|
|
399
|
-
headers:
|
|
400
|
-
query_string:
|
|
395
|
+
headers: dict[str, str] | None = None,
|
|
396
|
+
query_string: dict[str, str] | None = None,
|
|
401
397
|
**kwargs: Any,
|
|
402
398
|
) -> WSGIResponse:
|
|
403
399
|
from .transports.responses import WSGIResponse
|
|
@@ -426,8 +422,8 @@ class Case:
|
|
|
426
422
|
def call_asgi(
|
|
427
423
|
self,
|
|
428
424
|
app: Any = None,
|
|
429
|
-
base_url:
|
|
430
|
-
headers:
|
|
425
|
+
base_url: str | None = None,
|
|
426
|
+
headers: dict[str, str] | None = None,
|
|
431
427
|
**kwargs: Any,
|
|
432
428
|
) -> requests.Response:
|
|
433
429
|
from starlette_testclient import TestClient as ASGIClient
|
|
@@ -447,10 +443,10 @@ class Case:
|
|
|
447
443
|
def validate_response(
|
|
448
444
|
self,
|
|
449
445
|
response: GenericResponse,
|
|
450
|
-
checks:
|
|
451
|
-
additional_checks:
|
|
452
|
-
excluded_checks:
|
|
453
|
-
code_sample_style:
|
|
446
|
+
checks: tuple[CheckFunction, ...] = (),
|
|
447
|
+
additional_checks: tuple[CheckFunction, ...] = (),
|
|
448
|
+
excluded_checks: tuple[CheckFunction, ...] = (),
|
|
449
|
+
code_sample_style: str | None = None,
|
|
454
450
|
) -> None:
|
|
455
451
|
"""Validate application response.
|
|
456
452
|
|
|
@@ -536,11 +532,11 @@ class Case:
|
|
|
536
532
|
|
|
537
533
|
def call_and_validate(
|
|
538
534
|
self,
|
|
539
|
-
base_url:
|
|
540
|
-
session:
|
|
541
|
-
headers:
|
|
542
|
-
checks:
|
|
543
|
-
code_sample_style:
|
|
535
|
+
base_url: str | None = None,
|
|
536
|
+
session: requests.Session | None = None,
|
|
537
|
+
headers: dict[str, Any] | None = None,
|
|
538
|
+
checks: tuple[CheckFunction, ...] = (),
|
|
539
|
+
code_sample_style: str | None = None,
|
|
544
540
|
**kwargs: Any,
|
|
545
541
|
) -> requests.Response:
|
|
546
542
|
__tracebackhide__ = True
|
|
@@ -558,7 +554,7 @@ class Case:
|
|
|
558
554
|
prepared = requests.Session().prepare_request(request) # type: ignore
|
|
559
555
|
return cast(str, prepared.url)
|
|
560
556
|
|
|
561
|
-
def partial_deepcopy(self) ->
|
|
557
|
+
def partial_deepcopy(self) -> Case:
|
|
562
558
|
return self.__class__(
|
|
563
559
|
operation=self.operation.partial_deepcopy(),
|
|
564
560
|
data_generation_method=self.data_generation_method,
|
|
@@ -572,14 +568,14 @@ class Case:
|
|
|
572
568
|
)
|
|
573
569
|
|
|
574
570
|
|
|
575
|
-
def _merge_dict_to(data:
|
|
571
|
+
def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
|
|
576
572
|
original = data[data_key] or {}
|
|
577
573
|
for key, value in new.items():
|
|
578
574
|
original[key] = value
|
|
579
575
|
data[data_key] = original
|
|
580
576
|
|
|
581
577
|
|
|
582
|
-
def validate_vanilla_requests_kwargs(data:
|
|
578
|
+
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
|
583
579
|
"""Check arguments for `requests.Session.request`.
|
|
584
580
|
|
|
585
581
|
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
|
@@ -595,7 +591,7 @@ def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
|
|
|
595
591
|
|
|
596
592
|
|
|
597
593
|
@contextmanager
|
|
598
|
-
def cookie_handler(client: werkzeug.Client, cookies:
|
|
594
|
+
def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
|
|
599
595
|
"""Set cookies required for a call."""
|
|
600
596
|
if not cookies:
|
|
601
597
|
yield
|
|
@@ -631,13 +627,13 @@ class OperationDefinition(Generic[P, D]):
|
|
|
631
627
|
scope: str
|
|
632
628
|
parameters: Sequence[P]
|
|
633
629
|
|
|
634
|
-
def __contains__(self, item:
|
|
630
|
+
def __contains__(self, item: str | int) -> bool:
|
|
635
631
|
return item in self.resolved
|
|
636
632
|
|
|
637
|
-
def __getitem__(self, item:
|
|
633
|
+
def __getitem__(self, item: str | int) -> None | bool | float | str | list | dict[str, Any]:
|
|
638
634
|
return self.resolved[item]
|
|
639
635
|
|
|
640
|
-
def get(self, item:
|
|
636
|
+
def get(self, item: str | int, default: Any = None) -> None | bool | float | str | list | dict[str, Any]:
|
|
641
637
|
return self.resolved.get(item, default)
|
|
642
638
|
|
|
643
639
|
|
|
@@ -663,16 +659,16 @@ class APIOperation(Generic[P, C]):
|
|
|
663
659
|
path: str
|
|
664
660
|
method: str
|
|
665
661
|
definition: OperationDefinition = field(repr=False)
|
|
666
|
-
schema:
|
|
662
|
+
schema: BaseSchema
|
|
667
663
|
verbose_name: str = None # type: ignore
|
|
668
664
|
app: Any = None
|
|
669
|
-
base_url:
|
|
665
|
+
base_url: str | None = None
|
|
670
666
|
path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
671
667
|
headers: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
672
668
|
cookies: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
673
669
|
query: ParameterSet[P] = field(default_factory=ParameterSet)
|
|
674
670
|
body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
|
|
675
|
-
case_cls:
|
|
671
|
+
case_cls: type[C] = Case # type: ignore
|
|
676
672
|
|
|
677
673
|
def __post_init__(self) -> None:
|
|
678
674
|
if self.verbose_name is None:
|
|
@@ -683,14 +679,14 @@ class APIOperation(Generic[P, C]):
|
|
|
683
679
|
return self.schema.get_full_path(self.path)
|
|
684
680
|
|
|
685
681
|
@property
|
|
686
|
-
def links(self) ->
|
|
682
|
+
def links(self) -> dict[str, dict[str, Any]]:
|
|
687
683
|
return self.schema.get_links(self)
|
|
688
684
|
|
|
689
685
|
def iter_parameters(self) -> Iterator[P]:
|
|
690
686
|
"""Iterate over all operation's parameters."""
|
|
691
687
|
return chain(self.path_parameters, self.headers, self.cookies, self.query)
|
|
692
688
|
|
|
693
|
-
def _lookup_container(self, location: str) ->
|
|
689
|
+
def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
|
|
694
690
|
return {
|
|
695
691
|
"path": self.path_parameters,
|
|
696
692
|
"header": self.headers,
|
|
@@ -713,7 +709,7 @@ class APIOperation(Generic[P, C]):
|
|
|
713
709
|
if container is not None:
|
|
714
710
|
container.add(parameter)
|
|
715
711
|
|
|
716
|
-
def get_parameter(self, name: str, location: str) ->
|
|
712
|
+
def get_parameter(self, name: str, location: str) -> P | None:
|
|
717
713
|
container = self._lookup_container(location)
|
|
718
714
|
if container is not None:
|
|
719
715
|
return container.get(name)
|
|
@@ -721,13 +717,16 @@ class APIOperation(Generic[P, C]):
|
|
|
721
717
|
|
|
722
718
|
def as_strategy(
|
|
723
719
|
self,
|
|
724
|
-
hooks:
|
|
725
|
-
auth_storage:
|
|
720
|
+
hooks: HookDispatcher | None = None,
|
|
721
|
+
auth_storage: AuthStorage | None = None,
|
|
726
722
|
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
|
723
|
+
generation_config: GenerationConfig | None = None,
|
|
727
724
|
**kwargs: Any,
|
|
728
725
|
) -> st.SearchStrategy:
|
|
729
726
|
"""Turn this API operation into a Hypothesis strategy."""
|
|
730
|
-
strategy = self.schema.get_case_strategy(
|
|
727
|
+
strategy = self.schema.get_case_strategy(
|
|
728
|
+
self, hooks, auth_storage, data_generation_method, generation_config=generation_config, **kwargs
|
|
729
|
+
)
|
|
731
730
|
|
|
732
731
|
def _apply_hooks(dispatcher: HookDispatcher, _strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
|
|
733
732
|
context = HookContext(self)
|
|
@@ -750,17 +749,17 @@ class APIOperation(Generic[P, C]):
|
|
|
750
749
|
strategy = _apply_hooks(hooks, strategy)
|
|
751
750
|
return strategy
|
|
752
751
|
|
|
753
|
-
def get_security_requirements(self) ->
|
|
752
|
+
def get_security_requirements(self) -> list[str]:
|
|
754
753
|
return self.schema.get_security_requirements(self)
|
|
755
754
|
|
|
756
|
-
def get_strategies_from_examples(self) ->
|
|
755
|
+
def get_strategies_from_examples(self) -> list[st.SearchStrategy[Case]]:
|
|
757
756
|
"""Get examples from the API operation."""
|
|
758
757
|
return self.schema.get_strategies_from_examples(self)
|
|
759
758
|
|
|
760
|
-
def get_stateful_tests(self, response: GenericResponse, stateful:
|
|
759
|
+
def get_stateful_tests(self, response: GenericResponse, stateful: Stateful | None) -> Sequence[StatefulTest]:
|
|
761
760
|
return self.schema.get_stateful_tests(response, self, stateful)
|
|
762
761
|
|
|
763
|
-
def get_parameter_serializer(self, location: str) ->
|
|
762
|
+
def get_parameter_serializer(self, location: str) -> Callable | None:
|
|
764
763
|
"""Get a function that serializes parameters for the given location.
|
|
765
764
|
|
|
766
765
|
It handles serializing data into various `collectionFormat` options and similar.
|
|
@@ -768,13 +767,13 @@ class APIOperation(Generic[P, C]):
|
|
|
768
767
|
"""
|
|
769
768
|
return self.schema.get_parameter_serializer(self, location)
|
|
770
769
|
|
|
771
|
-
def prepare_multipart(self, form_data: FormData) ->
|
|
770
|
+
def prepare_multipart(self, form_data: FormData) -> tuple[list | None, dict[str, Any] | None]:
|
|
772
771
|
return self.schema.prepare_multipart(form_data, self)
|
|
773
772
|
|
|
774
|
-
def get_request_payload_content_types(self) ->
|
|
773
|
+
def get_request_payload_content_types(self) -> list[str]:
|
|
775
774
|
return self.schema.get_request_payload_content_types(self)
|
|
776
775
|
|
|
777
|
-
def partial_deepcopy(self) ->
|
|
776
|
+
def partial_deepcopy(self) -> APIOperation:
|
|
778
777
|
return self.__class__(
|
|
779
778
|
path=self.path, # string, immutable
|
|
780
779
|
method=self.method, # string, immutable
|
|
@@ -790,7 +789,7 @@ class APIOperation(Generic[P, C]):
|
|
|
790
789
|
body=fast_deepcopy(self.body),
|
|
791
790
|
)
|
|
792
791
|
|
|
793
|
-
def clone(self, **components: Any) ->
|
|
792
|
+
def clone(self, **components: Any) -> APIOperation:
|
|
794
793
|
"""Create a new instance of this API operation with updated components."""
|
|
795
794
|
return self.__class__(
|
|
796
795
|
path=self.path,
|
|
@@ -810,12 +809,12 @@ class APIOperation(Generic[P, C]):
|
|
|
810
809
|
def make_case(
|
|
811
810
|
self,
|
|
812
811
|
*,
|
|
813
|
-
path_parameters:
|
|
814
|
-
headers:
|
|
815
|
-
cookies:
|
|
816
|
-
query:
|
|
817
|
-
body:
|
|
818
|
-
media_type:
|
|
812
|
+
path_parameters: PathParameters | None = None,
|
|
813
|
+
headers: Headers | None = None,
|
|
814
|
+
cookies: Cookies | None = None,
|
|
815
|
+
query: Query | None = None,
|
|
816
|
+
body: Body | NotSet = NOT_SET,
|
|
817
|
+
media_type: str | None = None,
|
|
819
818
|
) -> C:
|
|
820
819
|
"""Create a new example for this API operation.
|
|
821
820
|
|
|
@@ -837,7 +836,7 @@ class APIOperation(Generic[P, C]):
|
|
|
837
836
|
path = self.path.replace("~", "~0").replace("/", "~1")
|
|
838
837
|
return f"#/paths/{path}/{self.method}"
|
|
839
838
|
|
|
840
|
-
def validate_response(self, response: GenericResponse) -> None:
|
|
839
|
+
def validate_response(self, response: GenericResponse) -> bool | None:
|
|
841
840
|
"""Validate API response for conformance.
|
|
842
841
|
|
|
843
842
|
:raises CheckFailed: If the response does not conform to the API schema.
|
|
@@ -852,10 +851,10 @@ class APIOperation(Generic[P, C]):
|
|
|
852
851
|
except CheckFailed:
|
|
853
852
|
return False
|
|
854
853
|
|
|
855
|
-
def get_raw_payload_schema(self, media_type: str) ->
|
|
854
|
+
def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
|
856
855
|
return self.schema._get_payload_schema(self.definition.raw, media_type)
|
|
857
856
|
|
|
858
|
-
def get_resolved_payload_schema(self, media_type: str) ->
|
|
857
|
+
def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
|
|
859
858
|
return self.schema._get_payload_schema(self.definition.resolved, media_type)
|
|
860
859
|
|
|
861
860
|
|
|
@@ -878,13 +877,13 @@ class Check:
|
|
|
878
877
|
|
|
879
878
|
name: str
|
|
880
879
|
value: Status
|
|
881
|
-
response:
|
|
880
|
+
response: GenericResponse | None
|
|
882
881
|
elapsed: float
|
|
883
882
|
example: Case
|
|
884
|
-
message:
|
|
883
|
+
message: str | None = None
|
|
885
884
|
# Failure-specific context
|
|
886
|
-
context:
|
|
887
|
-
request:
|
|
885
|
+
context: FailureContext | None = None
|
|
886
|
+
request: requests.PreparedRequest | None = None
|
|
888
887
|
|
|
889
888
|
|
|
890
889
|
@dataclass(repr=False)
|
|
@@ -893,11 +892,11 @@ class Request:
|
|
|
893
892
|
|
|
894
893
|
method: str
|
|
895
894
|
uri: str
|
|
896
|
-
body:
|
|
895
|
+
body: str | None
|
|
897
896
|
headers: Headers
|
|
898
897
|
|
|
899
898
|
@classmethod
|
|
900
|
-
def from_case(cls, case: Case, session: requests.Session) ->
|
|
899
|
+
def from_case(cls, case: Case, session: requests.Session) -> Request:
|
|
901
900
|
"""Create a new `Request` instance from `Case`."""
|
|
902
901
|
import requests
|
|
903
902
|
|
|
@@ -908,7 +907,7 @@ class Request:
|
|
|
908
907
|
return cls.from_prepared_request(prepared)
|
|
909
908
|
|
|
910
909
|
@classmethod
|
|
911
|
-
def from_prepared_request(cls, prepared: requests.PreparedRequest) ->
|
|
910
|
+
def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
|
|
912
911
|
"""A prepared request version is already stored in `requests.Response`."""
|
|
913
912
|
body = prepared.body
|
|
914
913
|
|
|
@@ -933,15 +932,15 @@ class Response:
|
|
|
933
932
|
|
|
934
933
|
status_code: int
|
|
935
934
|
message: str
|
|
936
|
-
headers:
|
|
937
|
-
body:
|
|
938
|
-
encoding:
|
|
935
|
+
headers: dict[str, list[str]]
|
|
936
|
+
body: str | None
|
|
937
|
+
encoding: str | None
|
|
939
938
|
http_version: str
|
|
940
939
|
elapsed: float
|
|
941
940
|
verify: bool
|
|
942
941
|
|
|
943
942
|
@classmethod
|
|
944
|
-
def from_requests(cls, response: requests.Response) ->
|
|
943
|
+
def from_requests(cls, response: requests.Response) -> Response:
|
|
945
944
|
"""Create a response from requests.Response."""
|
|
946
945
|
headers = {name: response.raw.headers.getlist(name) for name in response.raw.headers.keys()}
|
|
947
946
|
# Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
|
|
@@ -966,7 +965,7 @@ class Response:
|
|
|
966
965
|
)
|
|
967
966
|
|
|
968
967
|
@classmethod
|
|
969
|
-
def from_wsgi(cls, response: WSGIResponse, elapsed: float) ->
|
|
968
|
+
def from_wsgi(cls, response: WSGIResponse, elapsed: float) -> Response:
|
|
970
969
|
"""Create a response from WSGI response."""
|
|
971
970
|
from .transports.responses import get_reason
|
|
972
971
|
|
|
@@ -975,7 +974,7 @@ class Response:
|
|
|
975
974
|
# Note, this call ensures that `response.response` is a sequence, which is needed for comparison
|
|
976
975
|
data = response.get_data()
|
|
977
976
|
body = None if response.response == [] else serialize_payload(data)
|
|
978
|
-
encoding:
|
|
977
|
+
encoding: str | None
|
|
979
978
|
if body is not None:
|
|
980
979
|
# Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
|
|
981
980
|
encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
|
|
@@ -999,15 +998,13 @@ class Interaction:
|
|
|
999
998
|
|
|
1000
999
|
request: Request
|
|
1001
1000
|
response: Response
|
|
1002
|
-
checks:
|
|
1001
|
+
checks: list[Check]
|
|
1003
1002
|
status: Status
|
|
1004
1003
|
data_generation_method: DataGenerationMethod
|
|
1005
1004
|
recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
|
|
1006
1005
|
|
|
1007
1006
|
@classmethod
|
|
1008
|
-
def from_requests(
|
|
1009
|
-
cls, case: Case, response: requests.Response, status: Status, checks: List[Check]
|
|
1010
|
-
) -> "Interaction":
|
|
1007
|
+
def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
|
|
1011
1008
|
return cls(
|
|
1012
1009
|
request=Request.from_prepared_request(response.request),
|
|
1013
1010
|
response=Response.from_requests(response),
|
|
@@ -1021,11 +1018,11 @@ class Interaction:
|
|
|
1021
1018
|
cls,
|
|
1022
1019
|
case: Case,
|
|
1023
1020
|
response: WSGIResponse,
|
|
1024
|
-
headers:
|
|
1021
|
+
headers: dict[str, Any],
|
|
1025
1022
|
elapsed: float,
|
|
1026
1023
|
status: Status,
|
|
1027
|
-
checks:
|
|
1028
|
-
) ->
|
|
1024
|
+
checks: list[Check],
|
|
1025
|
+
) -> Interaction:
|
|
1029
1026
|
import requests
|
|
1030
1027
|
|
|
1031
1028
|
session = requests.Session()
|
|
@@ -1048,16 +1045,18 @@ class TestResult:
|
|
|
1048
1045
|
method: str
|
|
1049
1046
|
path: str
|
|
1050
1047
|
verbose_name: str
|
|
1051
|
-
data_generation_method:
|
|
1052
|
-
checks:
|
|
1053
|
-
errors:
|
|
1054
|
-
interactions:
|
|
1055
|
-
logs:
|
|
1048
|
+
data_generation_method: list[DataGenerationMethod]
|
|
1049
|
+
checks: list[Check] = field(default_factory=list)
|
|
1050
|
+
errors: list[Exception] = field(default_factory=list)
|
|
1051
|
+
interactions: list[Interaction] = field(default_factory=list)
|
|
1052
|
+
logs: list[LogRecord] = field(default_factory=list)
|
|
1056
1053
|
is_errored: bool = False
|
|
1057
1054
|
is_flaky: bool = False
|
|
1058
1055
|
is_skipped: bool = False
|
|
1056
|
+
skip_reason: str | None = None
|
|
1059
1057
|
is_executed: bool = False
|
|
1060
|
-
|
|
1058
|
+
# DEPRECATED: Seed is the same per test run
|
|
1059
|
+
seed: int | None = None
|
|
1061
1060
|
|
|
1062
1061
|
def mark_errored(self) -> None:
|
|
1063
1062
|
self.is_errored = True
|
|
@@ -1065,8 +1064,10 @@ class TestResult:
|
|
|
1065
1064
|
def mark_flaky(self) -> None:
|
|
1066
1065
|
self.is_flaky = True
|
|
1067
1066
|
|
|
1068
|
-
def mark_skipped(self) -> None:
|
|
1067
|
+
def mark_skipped(self, exc: SkipTest | unittest.case.SkipTest | None) -> None:
|
|
1069
1068
|
self.is_skipped = True
|
|
1069
|
+
if exc is not None:
|
|
1070
|
+
self.skip_reason = str(exc)
|
|
1070
1071
|
|
|
1071
1072
|
def mark_executed(self) -> None:
|
|
1072
1073
|
self.is_executed = True
|
|
@@ -1094,11 +1095,11 @@ class TestResult:
|
|
|
1094
1095
|
self,
|
|
1095
1096
|
name: str,
|
|
1096
1097
|
example: Case,
|
|
1097
|
-
response:
|
|
1098
|
+
response: GenericResponse | None,
|
|
1098
1099
|
elapsed: float,
|
|
1099
1100
|
message: str,
|
|
1100
|
-
context:
|
|
1101
|
-
request:
|
|
1101
|
+
context: FailureContext | None,
|
|
1102
|
+
request: requests.PreparedRequest | None = None,
|
|
1102
1103
|
) -> Check:
|
|
1103
1104
|
check = Check(
|
|
1104
1105
|
name=name,
|
|
@@ -1117,7 +1118,7 @@ class TestResult:
|
|
|
1117
1118
|
self.errors.append(exception)
|
|
1118
1119
|
|
|
1119
1120
|
def store_requests_response(
|
|
1120
|
-
self, case: Case, response: requests.Response, status: Status, checks:
|
|
1121
|
+
self, case: Case, response: requests.Response, status: Status, checks: list[Check]
|
|
1121
1122
|
) -> None:
|
|
1122
1123
|
self.interactions.append(Interaction.from_requests(case, response, status, checks))
|
|
1123
1124
|
|
|
@@ -1125,10 +1126,10 @@ class TestResult:
|
|
|
1125
1126
|
self,
|
|
1126
1127
|
case: Case,
|
|
1127
1128
|
response: WSGIResponse,
|
|
1128
|
-
headers:
|
|
1129
|
+
headers: dict[str, Any],
|
|
1129
1130
|
elapsed: float,
|
|
1130
1131
|
status: Status,
|
|
1131
|
-
checks:
|
|
1132
|
+
checks: list[Check],
|
|
1132
1133
|
) -> None:
|
|
1133
1134
|
self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
|
|
1134
1135
|
|
|
@@ -1139,9 +1140,10 @@ class TestResultSet:
|
|
|
1139
1140
|
|
|
1140
1141
|
__test__ = False
|
|
1141
1142
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1143
|
+
seed: int | None
|
|
1144
|
+
results: list[TestResult] = field(default_factory=list)
|
|
1145
|
+
generic_errors: list[OperationSchemaError] = field(default_factory=list)
|
|
1146
|
+
warnings: list[str] = field(default_factory=list)
|
|
1145
1147
|
|
|
1146
1148
|
def __iter__(self) -> Iterator[TestResult]:
|
|
1147
1149
|
return iter(self.results)
|
|
@@ -1186,9 +1188,9 @@ class TestResultSet:
|
|
|
1186
1188
|
return self._count(lambda result: result.has_errors or result.is_errored) + len(self.generic_errors)
|
|
1187
1189
|
|
|
1188
1190
|
@property
|
|
1189
|
-
def total(self) ->
|
|
1191
|
+
def total(self) -> dict[str, dict[str | Status, int]]:
|
|
1190
1192
|
"""An aggregated statistic about test results."""
|
|
1191
|
-
output:
|
|
1193
|
+
output: dict[str, dict[str | Status, int]] = {}
|
|
1192
1194
|
for item in self.results:
|
|
1193
1195
|
for check in item.checks:
|
|
1194
1196
|
output.setdefault(check.name, Counter())
|