schemathesis 4.0.0a12__py3-none-any.whl → 4.0.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 +9 -4
- schemathesis/auths.py +20 -30
- schemathesis/checks.py +5 -0
- schemathesis/cli/commands/run/__init__.py +9 -6
- schemathesis/cli/commands/run/handlers/output.py +13 -0
- schemathesis/cli/constants.py +1 -1
- schemathesis/config/_operations.py +16 -21
- schemathesis/config/_projects.py +5 -1
- schemathesis/core/errors.py +10 -17
- schemathesis/core/transport.py +81 -1
- schemathesis/engine/errors.py +1 -1
- schemathesis/generation/case.py +152 -28
- schemathesis/generation/hypothesis/builder.py +12 -12
- schemathesis/generation/overrides.py +11 -27
- schemathesis/generation/stateful/__init__.py +13 -0
- schemathesis/generation/stateful/state_machine.py +31 -108
- schemathesis/graphql/loaders.py +14 -4
- schemathesis/hooks.py +1 -4
- schemathesis/openapi/checks.py +82 -20
- schemathesis/openapi/generation/filters.py +9 -2
- schemathesis/openapi/loaders.py +14 -4
- schemathesis/pytest/lazy.py +4 -31
- schemathesis/pytest/plugin.py +21 -11
- schemathesis/schemas.py +153 -89
- schemathesis/specs/graphql/schemas.py +6 -6
- schemathesis/specs/openapi/_hypothesis.py +39 -14
- schemathesis/specs/openapi/checks.py +95 -34
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +2 -2
- schemathesis/specs/openapi/parameters.py +0 -3
- schemathesis/specs/openapi/schemas.py +6 -91
- schemathesis/specs/openapi/stateful/links.py +1 -63
- schemathesis/transport/requests.py +12 -1
- schemathesis/transport/serialization.py +0 -4
- schemathesis/transport/wsgi.py +7 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/METADATA +8 -10
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/RECORD +41 -41
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from schemathesis import errors, graphql, openapi, pytest
|
4
|
-
from schemathesis.auths import AuthContext, auth
|
4
|
+
from schemathesis.auths import AuthContext, AuthProvider, auth
|
5
5
|
from schemathesis.checks import CheckContext, CheckFunction, check
|
6
|
+
from schemathesis.config import SchemathesisConfig as Config
|
6
7
|
from schemathesis.core.transport import Response
|
7
8
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
8
|
-
from schemathesis.generation import GenerationMode
|
9
|
+
from schemathesis.generation import GenerationMode, stateful
|
9
10
|
from schemathesis.generation.case import Case
|
10
11
|
from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
|
11
12
|
from schemathesis.hooks import HookContext, hook
|
12
|
-
from schemathesis.schemas import BaseSchema
|
13
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
13
14
|
from schemathesis.transport import SerializationContext, serializer
|
14
15
|
|
15
16
|
__version__ = SCHEMATHESIS_VERSION
|
@@ -19,8 +20,11 @@ __all__ = [
|
|
19
20
|
# Core data structures
|
20
21
|
"Case",
|
21
22
|
"Response",
|
22
|
-
"
|
23
|
+
"APIOperation",
|
23
24
|
"BaseSchema",
|
25
|
+
"Config",
|
26
|
+
"GenerationMode",
|
27
|
+
"stateful",
|
24
28
|
# Public errors
|
25
29
|
"errors",
|
26
30
|
# Spec or usage specific namespaces
|
@@ -37,6 +41,7 @@ __all__ = [
|
|
37
41
|
# Auth
|
38
42
|
"auth",
|
39
43
|
"AuthContext",
|
44
|
+
"AuthProvider",
|
40
45
|
# Targeted Property-based Testing
|
41
46
|
"metric",
|
42
47
|
"MetricContext",
|
schemathesis/auths.py
CHANGED
@@ -69,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
|
69
69
|
|
70
70
|
@runtime_checkable
|
71
71
|
class AuthProvider(Generic[Auth], Protocol):
|
72
|
-
"""
|
72
|
+
"""Protocol for implementing custom authentication in API tests."""
|
73
73
|
|
74
|
-
def get(self, case: Case,
|
75
|
-
"""
|
74
|
+
def get(self, case: Case, ctx: AuthContext) -> Auth | None:
|
75
|
+
"""Obtain authentication data for the test case.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
case: Generated test case requiring authentication.
|
79
|
+
ctx: Authentication state and configuration.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Authentication data (e.g., token, credentials) or `None`.
|
76
83
|
|
77
|
-
:param Case case: Generated test case.
|
78
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
79
|
-
:return: Any authentication data you find useful for your use case. For example, it could be an access token.
|
80
84
|
"""
|
81
85
|
|
82
|
-
def set(self, case: Case, data: Auth,
|
83
|
-
"""
|
86
|
+
def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
|
87
|
+
"""Apply authentication data to the test case.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
case: Test case to modify.
|
91
|
+
data: Authentication data from the `get` method.
|
92
|
+
ctx: Authentication state and configuration.
|
84
93
|
|
85
|
-
:param Optional[Auth] data: Authentication data you got from the ``get`` method.
|
86
|
-
:param Case case: Generated test case.
|
87
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
88
94
|
"""
|
89
95
|
|
90
96
|
|
@@ -333,8 +339,9 @@ class AuthStorage(Generic[Auth]):
|
|
333
339
|
) -> None:
|
334
340
|
if not issubclass(provider_class, AuthProvider):
|
335
341
|
raise TypeError(
|
336
|
-
f"`{provider_class.__name__}`
|
337
|
-
f"
|
342
|
+
f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
|
343
|
+
f"Auth providers must have `get` and `set` methods. "
|
344
|
+
f"See `schemathesis.AuthProvider` documentation for examples."
|
338
345
|
)
|
339
346
|
provider: AuthProvider
|
340
347
|
# Apply caching if desired
|
@@ -389,23 +396,6 @@ class AuthStorage(Generic[Auth]):
|
|
389
396
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
390
397
|
cache_by_key: CacheKeyFunction | None = None,
|
391
398
|
) -> FilterableApplyAuth:
|
392
|
-
"""Register auth provider only on one test function.
|
393
|
-
|
394
|
-
:param Type[AuthProvider] provider_class: Authentication provider class.
|
395
|
-
:param Optional[int] refresh_interval: Cache duration in seconds.
|
396
|
-
|
397
|
-
.. code-block:: python
|
398
|
-
|
399
|
-
class Auth:
|
400
|
-
...
|
401
|
-
|
402
|
-
|
403
|
-
@schema.auth(Auth)
|
404
|
-
@schema.parametrize()
|
405
|
-
def test_api(case):
|
406
|
-
...
|
407
|
-
|
408
|
-
"""
|
409
399
|
filter_set = FilterSet()
|
410
400
|
|
411
401
|
def wrapper(test: Callable) -> Callable:
|
schemathesis/checks.py
CHANGED
@@ -91,6 +91,11 @@ class CheckContext:
|
|
91
91
|
CHECKS = Registry[CheckFunction]()
|
92
92
|
|
93
93
|
|
94
|
+
def load_all_checks() -> None:
|
95
|
+
# NOTE: Trigger registering all Open API checks
|
96
|
+
from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401, F403
|
97
|
+
|
98
|
+
|
94
99
|
def check(func: CheckFunction) -> CheckFunction:
|
95
100
|
"""Register a custom validation check to run against API responses.
|
96
101
|
|
@@ -6,7 +6,7 @@ from typing import Any, Callable
|
|
6
6
|
import click
|
7
7
|
from click.utils import LazyFile
|
8
8
|
|
9
|
-
from schemathesis.checks import CHECKS
|
9
|
+
from schemathesis.checks import CHECKS, load_all_checks
|
10
10
|
from schemathesis.cli.commands.run import executor, validation
|
11
11
|
from schemathesis.cli.commands.run.filters import with_filters
|
12
12
|
from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
@@ -30,8 +30,7 @@ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
|
30
30
|
from schemathesis.generation import GenerationMode
|
31
31
|
from schemathesis.generation.metrics import METRICS, MetricFunction
|
32
32
|
|
33
|
-
|
34
|
-
from schemathesis.specs.openapi.checks import * # noqa: F401, F403
|
33
|
+
load_all_checks()
|
35
34
|
|
36
35
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
37
36
|
|
@@ -477,10 +476,14 @@ def run(
|
|
477
476
|
no_color: bool = False,
|
478
477
|
**__kwargs: Any,
|
479
478
|
) -> None:
|
480
|
-
"""
|
479
|
+
"""Generate and run property-based tests against your API.
|
481
480
|
|
482
|
-
|
483
|
-
|
481
|
+
\b
|
482
|
+
LOCATION can be:
|
483
|
+
- Local file: ./openapi.json, ./schema.yaml, ./schema.graphql
|
484
|
+
- OpenAPI URL: https://api.example.com/openapi.json
|
485
|
+
- GraphQL URL: https://api.example.com/graphql/
|
486
|
+
""" # noqa: D301
|
484
487
|
if no_color and force_color:
|
485
488
|
raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
|
486
489
|
|
@@ -1039,6 +1039,7 @@ class OutputHandler(EventHandler):
|
|
1039
1039
|
assert self.stateful_tests_manager is not None
|
1040
1040
|
links_seen = {case.transition.id for case in event.recorder.cases.values() if case.transition is not None}
|
1041
1041
|
self.stateful_tests_manager.update(links_seen, event.status)
|
1042
|
+
self._check_stateful_warnings(ctx, event)
|
1042
1043
|
|
1043
1044
|
def _check_warnings(self, ctx: ExecutionContext, event: events.ScenarioFinished) -> None:
|
1044
1045
|
statistic = aggregate_status_codes(event.recorder.interactions.values())
|
@@ -1090,6 +1091,18 @@ class OutputHandler(EventHandler):
|
|
1090
1091
|
):
|
1091
1092
|
self.warnings.validation_mismatch.add(event.recorder.label)
|
1092
1093
|
|
1094
|
+
def _check_stateful_warnings(self, ctx: ExecutionContext, event: events.ScenarioFinished) -> None:
|
1095
|
+
# If stateful testing had successful responses for API operations that were marked with "missing_test_data"
|
1096
|
+
# warnings, then remove them from warnings
|
1097
|
+
for key, node in event.recorder.cases.items():
|
1098
|
+
if not self.warnings.missing_test_data:
|
1099
|
+
break
|
1100
|
+
if node.value.operation.label in self.warnings.missing_test_data and key in event.recorder.interactions:
|
1101
|
+
response = event.recorder.interactions[key].response
|
1102
|
+
if response is not None and response.status_code < 300:
|
1103
|
+
self.warnings.missing_test_data.remove(node.value.operation.label)
|
1104
|
+
continue
|
1105
|
+
|
1093
1106
|
def _on_interrupted(self, event: events.Interrupted) -> None:
|
1094
1107
|
from rich.padding import Padding
|
1095
1108
|
|
schemathesis/cli/constants.py
CHANGED
@@ -5,4 +5,4 @@ ISSUE_TRACKER_URL = (
|
|
5
5
|
"https://github.com/schemathesis/schemathesis/issues/new?"
|
6
6
|
"labels=Status%3A%20Needs%20Triage%2C+Type%3A+Bug&template=bug_report.md&title=%5BBUG%5D"
|
7
7
|
)
|
8
|
-
EXTENSIONS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/extending
|
8
|
+
EXTENSIONS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/extending/"
|
@@ -159,34 +159,23 @@ class OperationsConfig(DiffBase):
|
|
159
159
|
if exclude_deprecated:
|
160
160
|
exclude_set.include(is_deprecated)
|
161
161
|
|
162
|
-
|
163
|
-
if not include_set.is_empty():
|
164
|
-
self.operations.insert(0, OperationConfig(filter_set=include_set, enabled=True))
|
165
|
-
if not exclude_set.is_empty():
|
166
|
-
self.operations.insert(0, OperationConfig(filter_set=exclude_set, enabled=False))
|
162
|
+
operations = list(self.operations)
|
167
163
|
|
168
164
|
final = FilterSet()
|
169
165
|
|
170
|
-
# Get a stable reference to operations
|
171
|
-
operations = list(self.operations)
|
172
|
-
|
173
|
-
# Define a closure that implements our priority logic
|
174
166
|
def priority_filter(ctx: HasAPIOperation) -> bool:
|
175
167
|
"""Filter operations according to CLI and config priority."""
|
176
|
-
# 1. CLI includes override everything if present
|
177
|
-
if not include_set.is_empty():
|
178
|
-
return include_set.match(ctx)
|
179
|
-
|
180
|
-
# 2. CLI excludes take precedence over config
|
181
|
-
if not exclude_set.is_empty() and exclude_set.match(ctx):
|
182
|
-
return False
|
183
|
-
|
184
|
-
# 3. Check config operations in priority order (first match wins)
|
185
168
|
for op_config in operations:
|
186
|
-
if op_config._filter_set.match(ctx):
|
187
|
-
return
|
169
|
+
if op_config._filter_set.match(ctx) and not op_config.enabled:
|
170
|
+
return False
|
171
|
+
|
172
|
+
if not include_set.is_empty():
|
173
|
+
if exclude_set.is_empty():
|
174
|
+
return include_set.match(ctx)
|
175
|
+
return include_set.match(ctx) and not exclude_set.match(ctx)
|
176
|
+
elif not exclude_set.is_empty():
|
177
|
+
return not exclude_set.match(ctx)
|
188
178
|
|
189
|
-
# 4. Default to include if no rule matches
|
190
179
|
return True
|
191
180
|
|
192
181
|
# Add our priority function as the filter
|
@@ -278,24 +267,30 @@ class OperationConfig(DiffBase):
|
|
278
267
|
@classmethod
|
279
268
|
def from_dict(cls, data: dict[str, Any]) -> OperationConfig:
|
280
269
|
filter_set = FilterSet()
|
270
|
+
seen = set()
|
281
271
|
for key_suffix, arg_suffix in (("", ""), ("-regex", "_regex")):
|
282
272
|
for attr, arg_name in FILTER_ATTRIBUTES:
|
283
273
|
key = f"include-{attr}{key_suffix}"
|
284
274
|
if key in data:
|
275
|
+
seen.add(key)
|
285
276
|
with reraise_filter_error(attr):
|
286
277
|
filter_set.include(**{f"{arg_name}{arg_suffix}": data[key]})
|
287
278
|
key = f"exclude-{attr}{key_suffix}"
|
288
279
|
if key in data:
|
280
|
+
seen.add(key)
|
289
281
|
with reraise_filter_error(attr):
|
290
282
|
filter_set.exclude(**{f"{arg_name}{arg_suffix}": data[key]})
|
291
283
|
for key, method in (("include-by", filter_set.include), ("exclude-by", filter_set.exclude)):
|
292
284
|
if key in data:
|
285
|
+
seen.add(key)
|
293
286
|
expression = data[key]
|
294
287
|
try:
|
295
288
|
func = expression_to_filter_function(expression)
|
296
289
|
method(func)
|
297
290
|
except ValueError:
|
298
291
|
raise ConfigError(f"Invalid filter expression: '{expression}'") from None
|
292
|
+
if not set(data) - seen:
|
293
|
+
raise ConfigError("Operation filters defined, but no settings are being overridden")
|
299
294
|
|
300
295
|
return cls(
|
301
296
|
filter_set=filter_set,
|
schemathesis/config/_projects.py
CHANGED
@@ -192,6 +192,7 @@ class ProjectConfig(DiffBase):
|
|
192
192
|
tls_verify: bool | str | None = None,
|
193
193
|
request_cert: str | None = None,
|
194
194
|
request_cert_key: str | None = None,
|
195
|
+
parameters: dict[str, Any] | None = None,
|
195
196
|
proxy: str | None = None,
|
196
197
|
suppress_health_check: list[HealthCheck] | None = None,
|
197
198
|
warnings: bool | list[SchemathesisWarning] | None = None,
|
@@ -235,6 +236,9 @@ class ProjectConfig(DiffBase):
|
|
235
236
|
if proxy is not None:
|
236
237
|
self.proxy = proxy
|
237
238
|
|
239
|
+
if parameters is not None:
|
240
|
+
self.parameters = parameters
|
241
|
+
|
238
242
|
if suppress_health_check is not None:
|
239
243
|
self.suppress_health_check = suppress_health_check
|
240
244
|
|
@@ -251,7 +255,7 @@ class ProjectConfig(DiffBase):
|
|
251
255
|
return self.auth.basic
|
252
256
|
return None
|
253
257
|
|
254
|
-
def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str]
|
258
|
+
def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str]:
|
255
259
|
"""Get explicitly configured headers."""
|
256
260
|
headers = self.headers.copy() if self.headers else {}
|
257
261
|
if operation is not None:
|
schemathesis/core/errors.py
CHANGED
@@ -20,10 +20,16 @@ if TYPE_CHECKING:
|
|
20
20
|
|
21
21
|
|
22
22
|
SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
SERIALIZERS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/custom-serializers/"
|
24
|
+
SERIALIZERS_SUGGESTION_MESSAGE = f"Check your schema or add custom serializers: {SERIALIZERS_DOCUMENTATION_URL}"
|
25
|
+
SERIALIZATION_NOT_POSSIBLE_MESSAGE = f"No supported serializers for media types: {{}}\n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
26
|
+
SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
|
27
|
+
f"Cannot serialize to '{{}}' (unsupported media type)\n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
28
|
+
)
|
29
|
+
RECURSIVE_REFERENCE_ERROR_MESSAGE = (
|
30
|
+
"Currently, Schemathesis can't generate data for this operation due to "
|
31
|
+
"recursive references in the operation definition. See more information in "
|
32
|
+
"this issue - https://github.com/schemathesis/schemathesis/issues/947"
|
27
33
|
)
|
28
34
|
|
29
35
|
|
@@ -277,19 +283,6 @@ class UnboundPrefix(SerializationError):
|
|
277
283
|
super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
|
278
284
|
|
279
285
|
|
280
|
-
SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
|
281
|
-
f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
282
|
-
)
|
283
|
-
SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
|
284
|
-
f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
|
285
|
-
)
|
286
|
-
RECURSIVE_REFERENCE_ERROR_MESSAGE = (
|
287
|
-
"Currently, Schemathesis can't generate data for this operation due to "
|
288
|
-
"recursive references in the operation definition. See more information in "
|
289
|
-
"this issue - https://github.com/schemathesis/schemathesis/issues/947"
|
290
|
-
)
|
291
|
-
|
292
|
-
|
293
286
|
class SerializationNotPossible(SerializationError):
|
294
287
|
"""Not possible to serialize data to specified media type(s).
|
295
288
|
|
schemathesis/core/transport.py
CHANGED
@@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, Any, Mapping
|
|
7
7
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
10
|
+
import httpx
|
10
11
|
import requests
|
12
|
+
from werkzeug.test import TestResponse
|
13
|
+
|
14
|
+
from schemathesis.generation.overrides import Override
|
11
15
|
|
12
16
|
USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
|
13
17
|
DEFAULT_RESPONSE_TIMEOUT = 10
|
@@ -51,6 +55,7 @@ class Response:
|
|
51
55
|
"""HTTP protocol version ("1.0" or "1.1")."""
|
52
56
|
encoding: str | None
|
53
57
|
"""Character encoding for text content, if detected."""
|
58
|
+
_override: Override | None
|
54
59
|
|
55
60
|
__slots__ = (
|
56
61
|
"status_code",
|
@@ -64,6 +69,7 @@ class Response:
|
|
64
69
|
"http_version",
|
65
70
|
"encoding",
|
66
71
|
"_encoded_body",
|
72
|
+
"_override",
|
67
73
|
)
|
68
74
|
|
69
75
|
def __init__(
|
@@ -77,6 +83,7 @@ class Response:
|
|
77
83
|
message: str = "",
|
78
84
|
http_version: str = "1.1",
|
79
85
|
encoding: str | None = None,
|
86
|
+
_override: Override | None = None,
|
80
87
|
):
|
81
88
|
self.status_code = status_code
|
82
89
|
self.headers = {key.lower(): value for key, value in headers.items()}
|
@@ -90,9 +97,24 @@ class Response:
|
|
90
97
|
self.message = message
|
91
98
|
self.http_version = http_version
|
92
99
|
self.encoding = encoding
|
100
|
+
self._override = _override
|
101
|
+
|
102
|
+
@classmethod
|
103
|
+
def from_any(cls, response: Response | httpx.Response | requests.Response | TestResponse) -> Response:
|
104
|
+
import httpx
|
105
|
+
import requests
|
106
|
+
from werkzeug.test import TestResponse
|
107
|
+
|
108
|
+
if isinstance(response, requests.Response):
|
109
|
+
return Response.from_requests(response, verify=True)
|
110
|
+
elif isinstance(response, httpx.Response):
|
111
|
+
return Response.from_httpx(response, verify=True)
|
112
|
+
elif isinstance(response, TestResponse):
|
113
|
+
return Response.from_wsgi(response)
|
114
|
+
return response
|
93
115
|
|
94
116
|
@classmethod
|
95
|
-
def from_requests(cls, response: requests.Response, verify: bool) -> Response:
|
117
|
+
def from_requests(cls, response: requests.Response, verify: bool, _override: Override | None = None) -> Response:
|
96
118
|
raw = response.raw
|
97
119
|
raw_headers = raw.headers if raw is not None else {}
|
98
120
|
headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
|
@@ -109,6 +131,64 @@ class Response:
|
|
109
131
|
encoding=response.encoding,
|
110
132
|
http_version=http_version,
|
111
133
|
verify=verify,
|
134
|
+
_override=_override,
|
135
|
+
)
|
136
|
+
|
137
|
+
@classmethod
|
138
|
+
def from_httpx(cls, response: httpx.Response, verify: bool) -> Response:
|
139
|
+
import requests
|
140
|
+
|
141
|
+
request = requests.Request(
|
142
|
+
method=response.request.method,
|
143
|
+
url=str(response.request.url),
|
144
|
+
headers=dict(response.request.headers),
|
145
|
+
params=dict(response.request.url.params),
|
146
|
+
data=response.request.content,
|
147
|
+
).prepare()
|
148
|
+
return Response(
|
149
|
+
status_code=response.status_code,
|
150
|
+
headers={key: [value] for key, value in response.headers.items()},
|
151
|
+
content=response.content,
|
152
|
+
request=request,
|
153
|
+
elapsed=response.elapsed.total_seconds(),
|
154
|
+
message=response.reason_phrase,
|
155
|
+
encoding=response.encoding,
|
156
|
+
http_version=response.http_version,
|
157
|
+
verify=verify,
|
158
|
+
)
|
159
|
+
|
160
|
+
@classmethod
|
161
|
+
def from_wsgi(cls, response: TestResponse) -> Response:
|
162
|
+
import http.client
|
163
|
+
|
164
|
+
import requests
|
165
|
+
|
166
|
+
reason = http.client.responses.get(response.status_code, "Unknown")
|
167
|
+
data = response.get_data()
|
168
|
+
if response.response == []:
|
169
|
+
# Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
|
170
|
+
encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
|
171
|
+
else:
|
172
|
+
encoding = None
|
173
|
+
request = requests.Request(
|
174
|
+
method=response.request.method,
|
175
|
+
url=str(response.request.url),
|
176
|
+
headers=dict(response.request.headers),
|
177
|
+
params=dict(response.request.args),
|
178
|
+
# Request body is not available
|
179
|
+
data=b"",
|
180
|
+
).prepare()
|
181
|
+
return Response(
|
182
|
+
status_code=response.status_code,
|
183
|
+
headers={name: response.headers.getlist(name) for name in response.headers.keys()},
|
184
|
+
content=data,
|
185
|
+
request=request,
|
186
|
+
# Elapsed time is not available
|
187
|
+
elapsed=0.0,
|
188
|
+
message=reason,
|
189
|
+
encoding=encoding,
|
190
|
+
http_version="1.1",
|
191
|
+
verify=False,
|
112
192
|
)
|
113
193
|
|
114
194
|
@property
|
schemathesis/engine/errors.py
CHANGED
@@ -261,7 +261,7 @@ def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[s
|
|
261
261
|
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
262
262
|
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
263
263
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
264
|
-
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql
|
264
|
+
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/guides/graphql-custom-scalars/",
|
265
265
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
266
266
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
267
267
|
RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|