schemathesis 3.39.16__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import binascii
|
4
|
+
import inspect
|
5
|
+
import os
|
6
|
+
from io import BytesIO
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
from urllib.parse import urlparse
|
9
|
+
|
10
|
+
from schemathesis.core import NotSet
|
11
|
+
from schemathesis.core.rate_limit import ratelimit
|
12
|
+
from schemathesis.core.transforms import deepclone, merge_at
|
13
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT, Response
|
14
|
+
from schemathesis.generation.overrides import Override
|
15
|
+
from schemathesis.transport import BaseTransport, SerializationContext
|
16
|
+
from schemathesis.transport.prepare import prepare_body, prepare_headers, prepare_url
|
17
|
+
from schemathesis.transport.serialization import Binary, serialize_binary, serialize_json, serialize_xml, serialize_yaml
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
import requests
|
21
|
+
|
22
|
+
from schemathesis.generation.case import Case
|
23
|
+
|
24
|
+
|
25
|
+
class RequestsTransport(BaseTransport["requests.Session"]):
|
26
|
+
def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
|
27
|
+
base_url = kwargs.get("base_url")
|
28
|
+
headers = kwargs.get("headers")
|
29
|
+
params = kwargs.get("params")
|
30
|
+
cookies = kwargs.get("cookies")
|
31
|
+
|
32
|
+
final_headers = prepare_headers(case, headers)
|
33
|
+
|
34
|
+
media_type = case.media_type
|
35
|
+
|
36
|
+
# Set content type header if needed
|
37
|
+
if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
|
38
|
+
if "content-type" not in final_headers:
|
39
|
+
final_headers["Content-Type"] = media_type
|
40
|
+
|
41
|
+
url = prepare_url(case, base_url)
|
42
|
+
|
43
|
+
# Handle serialization
|
44
|
+
if not isinstance(case.body, NotSet) and media_type is not None:
|
45
|
+
serializer = self._get_serializer(media_type)
|
46
|
+
context = SerializationContext(case=case)
|
47
|
+
extra = serializer(context, prepare_body(case))
|
48
|
+
else:
|
49
|
+
extra = {}
|
50
|
+
|
51
|
+
if case._auth is not None:
|
52
|
+
extra["auth"] = case._auth
|
53
|
+
|
54
|
+
# Additional headers from serializer
|
55
|
+
additional_headers = extra.pop("headers", None)
|
56
|
+
if additional_headers:
|
57
|
+
for key, value in additional_headers.items():
|
58
|
+
final_headers.setdefault(key, value)
|
59
|
+
|
60
|
+
params = case.query
|
61
|
+
|
62
|
+
# Replace empty dictionaries with empty strings, so the parameters actually present in the query string
|
63
|
+
if any(value == {} for value in (params or {}).values()):
|
64
|
+
params = deepclone(params)
|
65
|
+
for key, value in params.items():
|
66
|
+
if value == {}:
|
67
|
+
params[key] = ""
|
68
|
+
|
69
|
+
data = {
|
70
|
+
"method": case.method,
|
71
|
+
"url": url,
|
72
|
+
"cookies": case.cookies,
|
73
|
+
"headers": final_headers,
|
74
|
+
"params": params,
|
75
|
+
**extra,
|
76
|
+
}
|
77
|
+
|
78
|
+
if params is not None:
|
79
|
+
merge_at(data, "params", params)
|
80
|
+
if cookies is not None:
|
81
|
+
merge_at(data, "cookies", cookies)
|
82
|
+
|
83
|
+
return data
|
84
|
+
|
85
|
+
def send(self, case: Case, *, session: requests.Session | None = None, **kwargs: Any) -> Response:
|
86
|
+
import requests
|
87
|
+
|
88
|
+
data = self.serialize_case(case, **kwargs)
|
89
|
+
kwargs.pop("base_url", None)
|
90
|
+
data.update({key: value for key, value in kwargs.items() if key not in data})
|
91
|
+
data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT)
|
92
|
+
|
93
|
+
if session is None:
|
94
|
+
validate_vanilla_requests_kwargs(data)
|
95
|
+
session = requests.Session()
|
96
|
+
session.headers = {}
|
97
|
+
close_session = True
|
98
|
+
else:
|
99
|
+
close_session = False
|
100
|
+
|
101
|
+
verify = data.get("verify", True)
|
102
|
+
|
103
|
+
try:
|
104
|
+
config = case.operation.schema.config
|
105
|
+
rate_limit = config.rate_limit_for(operation=case.operation)
|
106
|
+
with ratelimit(rate_limit, config.base_url):
|
107
|
+
response = session.request(**data) # type: ignore
|
108
|
+
return Response.from_requests(
|
109
|
+
response,
|
110
|
+
verify=verify,
|
111
|
+
_override=Override(
|
112
|
+
query=kwargs.get("params") or {},
|
113
|
+
headers=kwargs.get("headers") or {},
|
114
|
+
cookies=kwargs.get("cookies") or {},
|
115
|
+
path_parameters={},
|
116
|
+
),
|
117
|
+
)
|
118
|
+
|
119
|
+
finally:
|
120
|
+
if close_session:
|
121
|
+
session.close()
|
122
|
+
|
123
|
+
|
124
|
+
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
125
|
+
"""Check arguments for `requests.Session.request`.
|
126
|
+
|
127
|
+
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
128
|
+
`requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
|
129
|
+
"""
|
130
|
+
url = data["url"]
|
131
|
+
if not urlparse(url).netloc:
|
132
|
+
stack = inspect.stack()
|
133
|
+
method_name = "call"
|
134
|
+
for frame in stack[1:]:
|
135
|
+
if frame.function == "call_and_validate":
|
136
|
+
method_name = "call_and_validate"
|
137
|
+
break
|
138
|
+
raise RuntimeError(
|
139
|
+
"The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
|
140
|
+
f"Pass `base_url` either to the `schemathesis.openapi.from_*` loader or to the `Case.{method_name}`.\n"
|
141
|
+
f"If you use the ASGI integration, please supply your test client "
|
142
|
+
f"as the `session` argument to `call`.\nURL: {url}"
|
143
|
+
)
|
144
|
+
|
145
|
+
|
146
|
+
REQUESTS_TRANSPORT = RequestsTransport()
|
147
|
+
|
148
|
+
|
149
|
+
@REQUESTS_TRANSPORT.serializer("application/json", "text/json")
|
150
|
+
def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
151
|
+
return serialize_json(value)
|
152
|
+
|
153
|
+
|
154
|
+
@REQUESTS_TRANSPORT.serializer(
|
155
|
+
"text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
|
156
|
+
)
|
157
|
+
def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
158
|
+
return serialize_yaml(value)
|
159
|
+
|
160
|
+
|
161
|
+
def _should_coerce_to_bytes(item: Any) -> bool:
|
162
|
+
"""Whether the item should be converted to bytes."""
|
163
|
+
# These types are OK in forms, others should be coerced to bytes
|
164
|
+
return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
|
165
|
+
|
166
|
+
|
167
|
+
def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
|
168
|
+
"""Make the generated data suitable for sending as multipart.
|
169
|
+
|
170
|
+
If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
|
171
|
+
we convert it to bytes and send it as-is, ignoring any conversion errors.
|
172
|
+
|
173
|
+
NOTE. This behavior might change in the future.
|
174
|
+
"""
|
175
|
+
for name, value in data.items():
|
176
|
+
if isinstance(value, list):
|
177
|
+
data[name] = [serialize_binary(item) if _should_coerce_to_bytes(item) else item for item in value]
|
178
|
+
elif _should_coerce_to_bytes(value):
|
179
|
+
data[name] = serialize_binary(value)
|
180
|
+
return data
|
181
|
+
|
182
|
+
|
183
|
+
def choose_boundary() -> str:
|
184
|
+
"""Random boundary name."""
|
185
|
+
return binascii.hexlify(os.urandom(16)).decode("ascii")
|
186
|
+
|
187
|
+
|
188
|
+
def _encode_multipart(value: Any, boundary: str) -> bytes:
|
189
|
+
"""Encode any value as multipart.
|
190
|
+
|
191
|
+
NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
|
192
|
+
be used as multipart, in cases when the API schema dictates so.
|
193
|
+
"""
|
194
|
+
# For such cases we stringify the value and wrap it to a randomly-generated boundary
|
195
|
+
body = BytesIO()
|
196
|
+
body.write(f"--{boundary}\r\n".encode())
|
197
|
+
body.write(str(value).encode())
|
198
|
+
body.write(f"--{boundary}--\r\n".encode("latin-1"))
|
199
|
+
return body.getvalue()
|
200
|
+
|
201
|
+
|
202
|
+
@REQUESTS_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
|
203
|
+
def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
204
|
+
if isinstance(value, bytes):
|
205
|
+
return {"data": value}
|
206
|
+
if isinstance(value, dict):
|
207
|
+
value = deepclone(value)
|
208
|
+
multipart = _prepare_form_data(value)
|
209
|
+
files, data = ctx.case.operation.prepare_multipart(multipart)
|
210
|
+
return {"files": files, "data": data}
|
211
|
+
# Uncommon schema. For example - `{"type": "string"}`
|
212
|
+
boundary = choose_boundary()
|
213
|
+
raw_data = _encode_multipart(value, boundary)
|
214
|
+
content_type = f"multipart/form-data; boundary={boundary}"
|
215
|
+
return {"data": raw_data, "headers": {"Content-Type": content_type}}
|
216
|
+
|
217
|
+
|
218
|
+
@REQUESTS_TRANSPORT.serializer("application/xml", "text/xml")
|
219
|
+
def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
220
|
+
media_type = ctx.case.media_type
|
221
|
+
|
222
|
+
assert media_type is not None
|
223
|
+
|
224
|
+
raw_schema = ctx.case.operation.get_raw_payload_schema(media_type)
|
225
|
+
resolved_schema = ctx.case.operation.get_resolved_payload_schema(media_type)
|
226
|
+
|
227
|
+
return serialize_xml(value, raw_schema, resolved_schema)
|
228
|
+
|
229
|
+
|
230
|
+
@REQUESTS_TRANSPORT.serializer("application/x-www-form-urlencoded")
|
231
|
+
def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
232
|
+
return {"data": value}
|
233
|
+
|
234
|
+
|
235
|
+
@REQUESTS_TRANSPORT.serializer("text/plain")
|
236
|
+
def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
237
|
+
if isinstance(value, bytes):
|
238
|
+
return {"data": value}
|
239
|
+
return {"data": str(value).encode("utf8")}
|
240
|
+
|
241
|
+
|
242
|
+
@REQUESTS_TRANSPORT.serializer("application/octet-stream")
|
243
|
+
def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
244
|
+
return {"data": serialize_binary(value)}
|
@@ -1,14 +1,74 @@
|
|
1
|
-
"""XML serialization."""
|
2
|
-
|
3
1
|
from __future__ import annotations
|
4
2
|
|
5
3
|
import re
|
4
|
+
from dataclasses import dataclass
|
6
5
|
from io import StringIO
|
7
6
|
from typing import Any, Dict, List, Union
|
8
7
|
from unicodedata import normalize
|
9
8
|
|
10
|
-
from .
|
11
|
-
from .
|
9
|
+
from schemathesis.core.errors import UnboundPrefix
|
10
|
+
from schemathesis.core.transforms import deepclone, transform
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class Binary(str):
|
15
|
+
"""A wrapper around `bytes` to resolve OpenAPI and JSON Schema `format` discrepancies.
|
16
|
+
|
17
|
+
Treat `bytes` as a valid type, allowing generation of bytes for OpenAPI `format` values like `binary` or `file`
|
18
|
+
that JSON Schema expects to be strings.
|
19
|
+
"""
|
20
|
+
|
21
|
+
data: bytes
|
22
|
+
|
23
|
+
__slots__ = ("data",)
|
24
|
+
|
25
|
+
def __hash__(self) -> int:
|
26
|
+
return hash(self.data)
|
27
|
+
|
28
|
+
|
29
|
+
def serialize_json(value: Any) -> dict[str, Any]:
|
30
|
+
if isinstance(value, bytes):
|
31
|
+
# Possible to get via explicit examples, e.g. `externalValue`
|
32
|
+
return {"data": value}
|
33
|
+
if isinstance(value, Binary):
|
34
|
+
return {"data": value.data}
|
35
|
+
if value is None:
|
36
|
+
# If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
|
37
|
+
# argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
|
38
|
+
# Therefore we explicitly create such payload
|
39
|
+
return {"data": b"null"}
|
40
|
+
return {"json": value}
|
41
|
+
|
42
|
+
|
43
|
+
def _replace_binary(value: dict) -> dict:
|
44
|
+
return {key: value.data if isinstance(value, Binary) else value for key, value in value.items()}
|
45
|
+
|
46
|
+
|
47
|
+
def serialize_binary(value: Any) -> bytes:
|
48
|
+
"""Convert the input value to bytes and ignore any conversion errors."""
|
49
|
+
if isinstance(value, bytes):
|
50
|
+
return value
|
51
|
+
if isinstance(value, Binary):
|
52
|
+
return value.data
|
53
|
+
return str(value).encode(errors="ignore")
|
54
|
+
|
55
|
+
|
56
|
+
def serialize_yaml(value: Any) -> dict[str, Any]:
|
57
|
+
import yaml
|
58
|
+
|
59
|
+
try:
|
60
|
+
from yaml import CSafeDumper as SafeDumper
|
61
|
+
except ImportError:
|
62
|
+
from yaml import SafeDumper # type: ignore
|
63
|
+
|
64
|
+
if isinstance(value, bytes):
|
65
|
+
return {"data": value}
|
66
|
+
if isinstance(value, Binary):
|
67
|
+
return {"data": value.data}
|
68
|
+
if isinstance(value, (list, dict)):
|
69
|
+
value = transform(value, _replace_binary)
|
70
|
+
return {"data": yaml.dump(value, Dumper=SafeDumper)}
|
71
|
+
|
12
72
|
|
13
73
|
Primitive = Union[str, int, float, bool, None]
|
14
74
|
JSON = Union[Primitive, List, Dict[str, Any]]
|
@@ -16,14 +76,12 @@ DEFAULT_TAG_NAME = "data"
|
|
16
76
|
NAMESPACE_URL = "http://example.com/schema"
|
17
77
|
|
18
78
|
|
19
|
-
def
|
79
|
+
def serialize_xml(
|
80
|
+
value: Any, raw_schema: dict[str, Any] | None, resolved_schema: dict[str, Any] | None
|
81
|
+
) -> dict[str, Any]:
|
20
82
|
"""Serialize a generated Python object as an XML string.
|
21
83
|
|
22
84
|
Schemas may contain additional information for fine-tuned XML serialization.
|
23
|
-
|
24
|
-
:param value: Generated value
|
25
|
-
:param raw_schema: The payload definition with not resolved references.
|
26
|
-
:param resolved_schema: The payload definition with all references resolved.
|
27
85
|
"""
|
28
86
|
if isinstance(value, (bytes, str)):
|
29
87
|
return {"data": value}
|
@@ -64,7 +122,7 @@ def _validate_prefix(options: dict[str, Any], namespace_stack: list[str]) -> Non
|
|
64
122
|
try:
|
65
123
|
prefix = options["prefix"]
|
66
124
|
if prefix not in namespace_stack:
|
67
|
-
raise
|
125
|
+
raise UnboundPrefix(prefix)
|
68
126
|
except KeyError:
|
69
127
|
pass
|
70
128
|
|
@@ -149,7 +207,7 @@ def _write_array(buffer: StringIO, obj: list[JSON], tag: str, schema: dict[str,
|
|
149
207
|
_write_namespace(buffer, options)
|
150
208
|
buffer.write(">")
|
151
209
|
# In Open API `items` value should be an object and not an array
|
152
|
-
items =
|
210
|
+
items = deepclone((schema or {}).get("items", {}))
|
153
211
|
child_options = items.get("xml", {})
|
154
212
|
child_tag = child_options.get("name", tag)
|
155
213
|
if not is_namespace_specified and "namespace" in options:
|
@@ -0,0 +1,171 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
from contextlib import contextmanager
|
5
|
+
from typing import TYPE_CHECKING, Any, Generator
|
6
|
+
|
7
|
+
from schemathesis.core import NotSet
|
8
|
+
from schemathesis.core.rate_limit import ratelimit
|
9
|
+
from schemathesis.core.transforms import merge_at
|
10
|
+
from schemathesis.core.transport import Response
|
11
|
+
from schemathesis.generation.case import Case
|
12
|
+
from schemathesis.generation.overrides import Override
|
13
|
+
from schemathesis.python import wsgi
|
14
|
+
from schemathesis.transport import BaseTransport, SerializationContext
|
15
|
+
from schemathesis.transport.prepare import normalize_base_url, prepare_body, prepare_headers, prepare_path
|
16
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
17
|
+
from schemathesis.transport.serialization import serialize_binary, serialize_json, serialize_xml, serialize_yaml
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
import werkzeug
|
21
|
+
|
22
|
+
|
23
|
+
class WSGITransport(BaseTransport["werkzeug.Client"]):
|
24
|
+
def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
|
25
|
+
headers = kwargs.get("headers")
|
26
|
+
params = kwargs.get("params")
|
27
|
+
|
28
|
+
final_headers = prepare_headers(case, headers)
|
29
|
+
|
30
|
+
media_type = case.media_type
|
31
|
+
|
32
|
+
# Set content type for payload
|
33
|
+
if media_type and not isinstance(case.body, NotSet):
|
34
|
+
final_headers["Content-Type"] = media_type
|
35
|
+
|
36
|
+
extra: dict[str, Any]
|
37
|
+
# Handle serialization
|
38
|
+
if not isinstance(case.body, NotSet) and media_type is not None:
|
39
|
+
serializer = self._get_serializer(media_type)
|
40
|
+
context = SerializationContext(case=case)
|
41
|
+
extra = serializer(context, prepare_body(case))
|
42
|
+
else:
|
43
|
+
extra = {}
|
44
|
+
|
45
|
+
data = {
|
46
|
+
"method": case.method,
|
47
|
+
"path": case.operation.schema.get_full_path(prepare_path(case.path, case.path_parameters)),
|
48
|
+
# Convert to regular dict for Werkzeug compatibility
|
49
|
+
"headers": dict(final_headers),
|
50
|
+
"query_string": case.query,
|
51
|
+
**extra,
|
52
|
+
}
|
53
|
+
|
54
|
+
if params is not None:
|
55
|
+
merge_at(data, "query_string", params)
|
56
|
+
|
57
|
+
return data
|
58
|
+
|
59
|
+
def send(
|
60
|
+
self,
|
61
|
+
case: Case,
|
62
|
+
*,
|
63
|
+
session: werkzeug.Client | None = None,
|
64
|
+
**kwargs: Any,
|
65
|
+
) -> Response:
|
66
|
+
import requests
|
67
|
+
|
68
|
+
headers = kwargs.pop("headers", None)
|
69
|
+
params = kwargs.pop("params", None)
|
70
|
+
cookies = kwargs.pop("cookies", None)
|
71
|
+
application = kwargs.pop("app")
|
72
|
+
|
73
|
+
data = self.serialize_case(case, headers=headers, params=params)
|
74
|
+
data.update({key: value for key, value in kwargs.items() if key not in data})
|
75
|
+
|
76
|
+
client = session or wsgi.get_client(application)
|
77
|
+
cookies = {**(case.cookies or {}), **(cookies or {})}
|
78
|
+
|
79
|
+
config = case.operation.schema.config
|
80
|
+
rate_limit = config.rate_limit_for(operation=case.operation)
|
81
|
+
with cookie_handler(client, cookies), ratelimit(rate_limit, config.base_url):
|
82
|
+
start = time.monotonic()
|
83
|
+
response = client.open(**data)
|
84
|
+
elapsed = time.monotonic() - start
|
85
|
+
|
86
|
+
requests_kwargs = REQUESTS_TRANSPORT.serialize_case(
|
87
|
+
case,
|
88
|
+
base_url=normalize_base_url(case.operation.base_url),
|
89
|
+
headers=headers,
|
90
|
+
params=params,
|
91
|
+
cookies=cookies,
|
92
|
+
)
|
93
|
+
|
94
|
+
headers = {key: response.headers.getlist(key) for key in response.headers.keys()}
|
95
|
+
|
96
|
+
return Response(
|
97
|
+
status_code=response.status_code,
|
98
|
+
headers=headers,
|
99
|
+
content=response.get_data(),
|
100
|
+
request=requests.Request(**requests_kwargs).prepare(),
|
101
|
+
elapsed=elapsed,
|
102
|
+
verify=False,
|
103
|
+
_override=Override(
|
104
|
+
query=kwargs.get("params") or {},
|
105
|
+
headers=kwargs.get("headers") or {},
|
106
|
+
cookies=kwargs.get("cookies") or {},
|
107
|
+
path_parameters={},
|
108
|
+
),
|
109
|
+
)
|
110
|
+
|
111
|
+
|
112
|
+
@contextmanager
|
113
|
+
def cookie_handler(client: werkzeug.Client, cookies: dict[str, Any] | None) -> Generator[None, None, None]:
|
114
|
+
"""Set cookies required for a call."""
|
115
|
+
if not cookies:
|
116
|
+
yield
|
117
|
+
else:
|
118
|
+
for key, value in cookies.items():
|
119
|
+
client.set_cookie(key=key, value=value, domain="localhost")
|
120
|
+
yield
|
121
|
+
for key in cookies:
|
122
|
+
client.delete_cookie(key=key, domain="localhost")
|
123
|
+
|
124
|
+
|
125
|
+
WSGI_TRANSPORT = WSGITransport()
|
126
|
+
|
127
|
+
|
128
|
+
@WSGI_TRANSPORT.serializer("application/json", "text/json")
|
129
|
+
def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
130
|
+
return serialize_json(value)
|
131
|
+
|
132
|
+
|
133
|
+
@WSGI_TRANSPORT.serializer(
|
134
|
+
"text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
|
135
|
+
)
|
136
|
+
def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
137
|
+
return serialize_yaml(value)
|
138
|
+
|
139
|
+
|
140
|
+
@WSGI_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
|
141
|
+
def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
142
|
+
return {"data": value}
|
143
|
+
|
144
|
+
|
145
|
+
@WSGI_TRANSPORT.serializer("application/xml", "text/xml")
|
146
|
+
def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
147
|
+
media_type = ctx.case.media_type
|
148
|
+
|
149
|
+
assert media_type is not None
|
150
|
+
|
151
|
+
raw_schema = ctx.case.operation.get_raw_payload_schema(media_type)
|
152
|
+
resolved_schema = ctx.case.operation.get_resolved_payload_schema(media_type)
|
153
|
+
|
154
|
+
return serialize_xml(value, raw_schema, resolved_schema)
|
155
|
+
|
156
|
+
|
157
|
+
@WSGI_TRANSPORT.serializer("application/x-www-form-urlencoded")
|
158
|
+
def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
159
|
+
return {"data": value}
|
160
|
+
|
161
|
+
|
162
|
+
@WSGI_TRANSPORT.serializer("text/plain")
|
163
|
+
def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
164
|
+
if isinstance(value, bytes):
|
165
|
+
return {"data": value}
|
166
|
+
return {"data": str(value)}
|
167
|
+
|
168
|
+
|
169
|
+
@WSGI_TRANSPORT.serializer("application/octet-stream")
|
170
|
+
def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
171
|
+
return {"data": serialize_binary(value)}
|