schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -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 +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Mapping
|
|
6
|
+
|
|
7
|
+
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import httpx
|
|
11
|
+
import requests
|
|
12
|
+
from werkzeug.test import TestResponse
|
|
13
|
+
|
|
14
|
+
from schemathesis.generation.overrides import Override
|
|
15
|
+
|
|
16
|
+
USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
|
|
17
|
+
DEFAULT_RESPONSE_TIMEOUT = 10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prepare_urlencoded(data: Any) -> Any:
|
|
21
|
+
if isinstance(data, list):
|
|
22
|
+
output = []
|
|
23
|
+
for item in data:
|
|
24
|
+
if isinstance(item, dict):
|
|
25
|
+
for key, value in item.items():
|
|
26
|
+
output.append((key, value))
|
|
27
|
+
else:
|
|
28
|
+
output.append((item, "arbitrary-value"))
|
|
29
|
+
return output
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Response:
|
|
34
|
+
"""HTTP response wrapper that normalizes different transport implementations.
|
|
35
|
+
|
|
36
|
+
Provides a consistent interface for accessing response data whether the request
|
|
37
|
+
was made via HTTP, ASGI, or WSGI transports.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
status_code: int
|
|
41
|
+
"""HTTP status code (e.g., 200, 404, 500)."""
|
|
42
|
+
headers: dict[str, list[str]]
|
|
43
|
+
"""Response headers with lowercase keys and list values."""
|
|
44
|
+
content: bytes
|
|
45
|
+
"""Raw response body as bytes."""
|
|
46
|
+
request: requests.PreparedRequest
|
|
47
|
+
"""The request that generated this response."""
|
|
48
|
+
elapsed: float
|
|
49
|
+
"""Response time in seconds."""
|
|
50
|
+
verify: bool
|
|
51
|
+
"""Whether TLS verification was enabled for the request."""
|
|
52
|
+
message: str
|
|
53
|
+
"""HTTP status message (e.g., "OK", "Not Found")."""
|
|
54
|
+
http_version: str
|
|
55
|
+
"""HTTP protocol version ("1.0" or "1.1")."""
|
|
56
|
+
encoding: str | None
|
|
57
|
+
"""Character encoding for text content, if detected."""
|
|
58
|
+
_override: Override | None
|
|
59
|
+
|
|
60
|
+
__slots__ = (
|
|
61
|
+
"status_code",
|
|
62
|
+
"headers",
|
|
63
|
+
"content",
|
|
64
|
+
"request",
|
|
65
|
+
"elapsed",
|
|
66
|
+
"verify",
|
|
67
|
+
"_json",
|
|
68
|
+
"message",
|
|
69
|
+
"http_version",
|
|
70
|
+
"encoding",
|
|
71
|
+
"_encoded_body",
|
|
72
|
+
"_override",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
status_code: int,
|
|
78
|
+
headers: Mapping[str, list[str]],
|
|
79
|
+
content: bytes,
|
|
80
|
+
request: requests.PreparedRequest,
|
|
81
|
+
elapsed: float,
|
|
82
|
+
verify: bool,
|
|
83
|
+
message: str = "",
|
|
84
|
+
http_version: str = "1.1",
|
|
85
|
+
encoding: str | None = None,
|
|
86
|
+
_override: Override | None = None,
|
|
87
|
+
):
|
|
88
|
+
self.status_code = status_code
|
|
89
|
+
self.headers = {key.lower(): value for key, value in headers.items()}
|
|
90
|
+
assert all(isinstance(v, list) for v in headers.values())
|
|
91
|
+
self.content = content
|
|
92
|
+
self.request = request
|
|
93
|
+
self.elapsed = elapsed
|
|
94
|
+
self.verify = verify
|
|
95
|
+
self._json = None
|
|
96
|
+
self._encoded_body: str | None = None
|
|
97
|
+
self.message = message
|
|
98
|
+
self.http_version = http_version
|
|
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
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_requests(cls, response: requests.Response, verify: bool, _override: Override | None = None) -> Response:
|
|
118
|
+
raw = response.raw
|
|
119
|
+
raw_headers = raw.headers if raw is not None else {}
|
|
120
|
+
headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
|
|
121
|
+
# Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
|
|
122
|
+
version = raw.version if raw is not None else 10
|
|
123
|
+
http_version = "1.0" if version == 10 else "1.1"
|
|
124
|
+
return Response(
|
|
125
|
+
status_code=response.status_code,
|
|
126
|
+
headers=headers,
|
|
127
|
+
content=response.content,
|
|
128
|
+
request=response.request,
|
|
129
|
+
elapsed=response.elapsed.total_seconds(),
|
|
130
|
+
message=response.reason,
|
|
131
|
+
encoding=response.encoding,
|
|
132
|
+
http_version=http_version,
|
|
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,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def text(self) -> str:
|
|
196
|
+
"""Decode response content as text using the detected or default encoding."""
|
|
197
|
+
return self.content.decode(self.encoding if self.encoding else "utf-8")
|
|
198
|
+
|
|
199
|
+
def json(self) -> Any:
|
|
200
|
+
"""Parse response content as JSON.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Parsed JSON data (dict, list, or primitive types)
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
json.JSONDecodeError: If content is not valid JSON
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
if self._json is None:
|
|
210
|
+
self._json = json.loads(self.text)
|
|
211
|
+
return self._json
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def body_size(self) -> int | None:
|
|
215
|
+
"""Size of response body in bytes, or None if no content."""
|
|
216
|
+
return len(self.content) if self.content else None
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def encoded_body(self) -> str | None:
|
|
220
|
+
"""Base64-encoded response body for binary-safe serialization."""
|
|
221
|
+
if self._encoded_body is None and self.content:
|
|
222
|
+
self._encoded_body = base64.b64encode(self.content).decode()
|
|
223
|
+
return self._encoded_body
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from schemathesis.core.errors import InvalidSchema
|
|
5
|
+
|
|
6
|
+
# Adapted from http.client._is_illegal_header_value
|
|
7
|
+
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def has_invalid_characters(name: str, value: object) -> bool:
|
|
11
|
+
from requests.exceptions import InvalidHeader
|
|
12
|
+
from requests.utils import check_header_validity
|
|
13
|
+
|
|
14
|
+
if not isinstance(value, str):
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
check_header_validity((name, value))
|
|
18
|
+
return bool(INVALID_HEADER_RE.search(value))
|
|
19
|
+
except InvalidHeader:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_latin_1_encodable(value: object) -> bool:
|
|
24
|
+
"""Check if a value is a Latin-1 encodable string."""
|
|
25
|
+
if not isinstance(value, str):
|
|
26
|
+
return False
|
|
27
|
+
try:
|
|
28
|
+
value.encode("latin-1")
|
|
29
|
+
return True
|
|
30
|
+
except UnicodeEncodeError:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_header_name(name: str) -> None:
|
|
35
|
+
from requests.exceptions import InvalidHeader
|
|
36
|
+
from requests.utils import check_header_validity
|
|
37
|
+
|
|
38
|
+
if not name:
|
|
39
|
+
raise InvalidSchema("Header name should not be empty")
|
|
40
|
+
if not name.isascii():
|
|
41
|
+
# `urllib3` encodes header names to ASCII
|
|
42
|
+
raise InvalidSchema(f"Header name should be ASCII: {name}")
|
|
43
|
+
try:
|
|
44
|
+
check_header_validity((name, ""))
|
|
45
|
+
except InvalidHeader as exc:
|
|
46
|
+
raise InvalidSchema(str(exc)) from None
|
|
47
|
+
if bool(INVALID_HEADER_RE.search(name)):
|
|
48
|
+
raise InvalidSchema(f"Invalid header name: {name}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
|
|
52
|
+
_contains_surrogate_pair = SURROGATE_PAIR_RE.search
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def contains_unicode_surrogate_pair(item: object) -> bool:
|
|
56
|
+
if isinstance(item, list):
|
|
57
|
+
return any(isinstance(item_, str) and bool(_contains_surrogate_pair(item_)) for item_ in item)
|
|
58
|
+
return isinstance(item, str) and bool(_contains_surrogate_pair(item))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INVALID_BASE_URL_MESSAGE = (
|
|
62
|
+
"The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
|
|
63
|
+
"Make sure it is a properly formatted URL."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_base_url(value: str) -> None:
|
|
68
|
+
try:
|
|
69
|
+
netloc = urlparse(value).netloc
|
|
70
|
+
except ValueError as exc:
|
|
71
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
|
|
72
|
+
if value and not netloc:
|
|
73
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from schemathesis.engine.core import Engine
|
|
8
|
+
from schemathesis.schemas import BaseSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Status(str, Enum):
|
|
12
|
+
SUCCESS = "success"
|
|
13
|
+
FAILURE = "failure"
|
|
14
|
+
ERROR = "error"
|
|
15
|
+
INTERRUPTED = "interrupted"
|
|
16
|
+
SKIP = "skip"
|
|
17
|
+
|
|
18
|
+
def __lt__(self, other: Status) -> bool: # type: ignore[override]
|
|
19
|
+
return _STATUS_ORDER[self] < _STATUS_ORDER[other]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_STATUS_ORDER = {Status.SUCCESS: 0, Status.FAILURE: 1, Status.ERROR: 2, Status.INTERRUPTED: 3, Status.SKIP: 4}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def from_schema(schema: BaseSchema) -> Engine:
|
|
26
|
+
from .core import Engine
|
|
27
|
+
|
|
28
|
+
return Engine(schema=schema)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from schemathesis.config import ProjectConfig
|
|
8
|
+
from schemathesis.core import NOT_SET, NotSet
|
|
9
|
+
from schemathesis.engine.control import ExecutionControl
|
|
10
|
+
from schemathesis.engine.observations import Observations
|
|
11
|
+
from schemathesis.generation.case import Case
|
|
12
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import threading
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class EngineContext:
|
|
24
|
+
"""Holds context shared for a test run."""
|
|
25
|
+
|
|
26
|
+
schema: BaseSchema
|
|
27
|
+
control: ExecutionControl
|
|
28
|
+
outcome_cache: dict[int, BaseException | None]
|
|
29
|
+
start_time: float
|
|
30
|
+
observations: Observations | None
|
|
31
|
+
|
|
32
|
+
__slots__ = (
|
|
33
|
+
"schema",
|
|
34
|
+
"control",
|
|
35
|
+
"outcome_cache",
|
|
36
|
+
"start_time",
|
|
37
|
+
"observations",
|
|
38
|
+
"_session",
|
|
39
|
+
"_transport_kwargs_cache",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
schema: BaseSchema,
|
|
46
|
+
stop_event: threading.Event,
|
|
47
|
+
observations: Observations | None = None,
|
|
48
|
+
session: requests.Session | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.schema = schema
|
|
51
|
+
self.control = ExecutionControl(stop_event=stop_event, max_failures=schema.config.max_failures)
|
|
52
|
+
self.outcome_cache = {}
|
|
53
|
+
self.start_time = time.monotonic()
|
|
54
|
+
self.observations = observations
|
|
55
|
+
self._session = session
|
|
56
|
+
self._transport_kwargs_cache: dict[str | None, dict[str, Any]] = {}
|
|
57
|
+
|
|
58
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def config(self) -> ProjectConfig:
|
|
62
|
+
return self.schema.config
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def running_time(self) -> float:
|
|
66
|
+
return time.monotonic() - self.start_time
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def has_to_stop(self) -> bool:
|
|
70
|
+
"""Check if execution should stop."""
|
|
71
|
+
return self.control.is_stopped
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def is_interrupted(self) -> bool:
|
|
75
|
+
return self.control.is_interrupted
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def has_reached_the_failure_limit(self) -> bool:
|
|
79
|
+
return self.control.has_reached_the_failure_limit
|
|
80
|
+
|
|
81
|
+
def record_observations(self, recorder: ScenarioRecorder) -> None:
|
|
82
|
+
"""Add new observations from a scenario."""
|
|
83
|
+
if self.observations is not None:
|
|
84
|
+
self.observations.extract_observations_from(recorder)
|
|
85
|
+
|
|
86
|
+
def inject_links(self) -> int:
|
|
87
|
+
"""Inject inferred OpenAPI links into API operations based on collected observations."""
|
|
88
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
89
|
+
|
|
90
|
+
injected = 0
|
|
91
|
+
if self.observations is not None and self.observations.location_headers:
|
|
92
|
+
assert isinstance(self.schema, BaseOpenAPISchema)
|
|
93
|
+
|
|
94
|
+
# Generate links from collected Location headers
|
|
95
|
+
for operation, entries in self.observations.location_headers.items():
|
|
96
|
+
injected += self.schema.analysis.inferencer.inject_links(operation.responses, entries)
|
|
97
|
+
if isinstance(self.schema, BaseOpenAPISchema) and self.schema.analysis.should_inject_links():
|
|
98
|
+
injected += self.schema.analysis.inject_links()
|
|
99
|
+
return injected
|
|
100
|
+
|
|
101
|
+
def stop(self) -> None:
|
|
102
|
+
self.control.stop()
|
|
103
|
+
|
|
104
|
+
def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
|
105
|
+
self.outcome_cache[hash(case)] = outcome
|
|
106
|
+
|
|
107
|
+
def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
|
|
108
|
+
return self.outcome_cache.get(hash(case), NOT_SET)
|
|
109
|
+
|
|
110
|
+
def get_session(self, *, operation: APIOperation | None = None) -> requests.Session:
|
|
111
|
+
if self._session is not None:
|
|
112
|
+
return self._session
|
|
113
|
+
import requests
|
|
114
|
+
|
|
115
|
+
session = requests.Session()
|
|
116
|
+
session.headers = {}
|
|
117
|
+
config = self.config
|
|
118
|
+
|
|
119
|
+
session.verify = config.tls_verify_for(operation=operation)
|
|
120
|
+
auth = config.auth_for(operation=operation)
|
|
121
|
+
if auth is not None:
|
|
122
|
+
session.auth = auth
|
|
123
|
+
headers = config.headers_for(operation=operation)
|
|
124
|
+
if headers:
|
|
125
|
+
session.headers.update(headers)
|
|
126
|
+
request_cert = config.request_cert_for(operation=operation)
|
|
127
|
+
if request_cert is not None:
|
|
128
|
+
session.cert = request_cert
|
|
129
|
+
proxy = config.proxy_for(operation=operation)
|
|
130
|
+
if proxy is not None:
|
|
131
|
+
session.proxies["all"] = proxy
|
|
132
|
+
return session
|
|
133
|
+
|
|
134
|
+
def get_transport_kwargs(self, operation: APIOperation | None = None) -> dict[str, Any]:
|
|
135
|
+
key = operation.label if operation is not None else None
|
|
136
|
+
cached = self._transport_kwargs_cache.get(key)
|
|
137
|
+
if cached is not None:
|
|
138
|
+
return cached.copy()
|
|
139
|
+
config = self.config
|
|
140
|
+
kwargs: dict[str, Any] = {
|
|
141
|
+
"session": self.get_session(operation=operation),
|
|
142
|
+
"headers": config.headers_for(operation=operation),
|
|
143
|
+
"max_redirects": config.max_redirects_for(operation=operation),
|
|
144
|
+
"timeout": config.request_timeout_for(operation=operation),
|
|
145
|
+
"verify": config.tls_verify_for(operation=operation),
|
|
146
|
+
"cert": config.request_cert_for(operation=operation),
|
|
147
|
+
}
|
|
148
|
+
proxy = config.proxy_for(operation=operation)
|
|
149
|
+
if proxy is not None:
|
|
150
|
+
kwargs["proxies"] = {"all": proxy}
|
|
151
|
+
self._transport_kwargs_cache[key] = kwargs
|
|
152
|
+
return kwargs
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Control for the Schemathesis Engine execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ExecutionControl:
|
|
11
|
+
"""Controls engine execution flow and tracks failures."""
|
|
12
|
+
|
|
13
|
+
stop_event: threading.Event
|
|
14
|
+
max_failures: int | None
|
|
15
|
+
_failures_counter: int
|
|
16
|
+
has_reached_the_failure_limit: bool
|
|
17
|
+
|
|
18
|
+
__slots__ = ("stop_event", "max_failures", "_failures_counter", "has_reached_the_failure_limit")
|
|
19
|
+
|
|
20
|
+
def __init__(self, stop_event: threading.Event, max_failures: int | None) -> None:
|
|
21
|
+
self.stop_event = stop_event
|
|
22
|
+
self.max_failures = max_failures
|
|
23
|
+
self._failures_counter = 0
|
|
24
|
+
self.has_reached_the_failure_limit = False
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_stopped(self) -> bool:
|
|
28
|
+
"""Check if execution should stop."""
|
|
29
|
+
return self.is_interrupted or self.has_reached_the_failure_limit
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_interrupted(self) -> bool:
|
|
33
|
+
return self.stop_event.is_set()
|
|
34
|
+
|
|
35
|
+
def stop(self) -> None:
|
|
36
|
+
"""Signal to stop execution."""
|
|
37
|
+
self.stop_event.set()
|
|
38
|
+
|
|
39
|
+
def count_failure(self) -> None:
|
|
40
|
+
# N failures limit
|
|
41
|
+
if self.max_failures is not None:
|
|
42
|
+
self._failures_counter += 1
|
|
43
|
+
if self._failures_counter >= self.max_failures:
|
|
44
|
+
self.has_reached_the_failure_limit = True
|