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,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from inspect import iscoroutinefunction
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
from schemathesis.core import media_types
|
|
8
|
+
from schemathesis.core.errors import SerializationNotPossible
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from schemathesis.core.transport import Response
|
|
12
|
+
from schemathesis.generation.case import Case
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get(app: Any) -> BaseTransport:
|
|
16
|
+
"""Get transport to send the data to the application."""
|
|
17
|
+
from schemathesis.transport.asgi import ASGI_TRANSPORT
|
|
18
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
|
19
|
+
from schemathesis.transport.wsgi import WSGI_TRANSPORT
|
|
20
|
+
|
|
21
|
+
if app is None:
|
|
22
|
+
return REQUESTS_TRANSPORT
|
|
23
|
+
if iscoroutinefunction(app) or (
|
|
24
|
+
hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
|
|
25
|
+
):
|
|
26
|
+
return ASGI_TRANSPORT
|
|
27
|
+
return WSGI_TRANSPORT
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
S = TypeVar("S", contravariant=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SerializationContext:
|
|
35
|
+
"""Context object passed to serializer functions.
|
|
36
|
+
|
|
37
|
+
It provides access to the generated test case and any related metadata.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
case: Case
|
|
41
|
+
"""The generated test case."""
|
|
42
|
+
|
|
43
|
+
__slots__ = ("case",)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Serializer = Callable[[SerializationContext, Any], Any]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BaseTransport(Generic[S]):
|
|
50
|
+
"""Base implementation with serializer registration."""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._serializers: dict[str, Serializer] = {}
|
|
54
|
+
|
|
55
|
+
def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
|
|
56
|
+
"""Prepare the case for sending."""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
def send(self, case: Case, *, session: S | None = None, **kwargs: Any) -> Response:
|
|
60
|
+
"""Send the case using this transport."""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def serializer(self, *media_types: str) -> Callable[[Serializer], Serializer]:
|
|
64
|
+
"""Register a serializer for given media types."""
|
|
65
|
+
|
|
66
|
+
def decorator(func: Serializer) -> Serializer:
|
|
67
|
+
for media_type in media_types:
|
|
68
|
+
self._serializers[media_type] = func
|
|
69
|
+
return func
|
|
70
|
+
|
|
71
|
+
return decorator
|
|
72
|
+
|
|
73
|
+
def unregister_serializer(self, *media_types: str) -> None:
|
|
74
|
+
for media_type in media_types:
|
|
75
|
+
self._serializers.pop(media_type, None)
|
|
76
|
+
|
|
77
|
+
def _copy_serializers_from(self, transport: BaseTransport) -> None:
|
|
78
|
+
self._serializers.update(transport._serializers)
|
|
79
|
+
|
|
80
|
+
def get_first_matching_media_type(self, media_type: str) -> tuple[str, Serializer] | None:
|
|
81
|
+
return next(self.get_matching_media_types(media_type), None)
|
|
82
|
+
|
|
83
|
+
def get_matching_media_types(self, media_type: str) -> Iterator[tuple[str, Serializer]]:
|
|
84
|
+
"""Get all registered media types matching the given media type."""
|
|
85
|
+
if media_type == "*/*":
|
|
86
|
+
# Shortcut to avoid comparing all values
|
|
87
|
+
yield from iter(self._serializers.items())
|
|
88
|
+
else:
|
|
89
|
+
main, sub = media_types.parse(media_type)
|
|
90
|
+
checks = [
|
|
91
|
+
media_types.is_json,
|
|
92
|
+
media_types.is_xml,
|
|
93
|
+
media_types.is_plain_text,
|
|
94
|
+
media_types.is_yaml,
|
|
95
|
+
]
|
|
96
|
+
for registered_media_type, serializer in self._serializers.items():
|
|
97
|
+
# Try known variations for popular media types and fallback to comparison
|
|
98
|
+
if any(check(media_type) and check(registered_media_type) for check in checks):
|
|
99
|
+
yield media_type, serializer
|
|
100
|
+
else:
|
|
101
|
+
target_main, target_sub = media_types.parse(registered_media_type)
|
|
102
|
+
if main in ("*", target_main) and sub in ("*", target_sub):
|
|
103
|
+
yield registered_media_type, serializer
|
|
104
|
+
|
|
105
|
+
def _get_serializer(self, input_media_type: str) -> Serializer:
|
|
106
|
+
pair = self.get_first_matching_media_type(input_media_type)
|
|
107
|
+
if pair is None:
|
|
108
|
+
# This media type is set manually. Otherwise, it should have been rejected during the data generation
|
|
109
|
+
raise SerializationNotPossible.for_media_type(input_media_type)
|
|
110
|
+
return pair[1]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_Serializer = Callable[[SerializationContext, Any], Union[bytes, None]]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SerializerRegistry:
|
|
117
|
+
"""Registry for serializers with aliasing support."""
|
|
118
|
+
|
|
119
|
+
def __call__(self, *media_types: str) -> Callable[[_Serializer], None]:
|
|
120
|
+
"""Register a serializer for specified media types on HTTP, ASGI, and WSGI transports.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
*media_types: One or more MIME types (e.g., "application/json") this serializer handles.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A decorator that wraps a function taking `(ctx: SerializationContext, value: Any)` and returning `bytes` for serialized body and `None` for omitting request body.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
```python
|
|
130
|
+
@schemathesis.serializer("text/csv")
|
|
131
|
+
def csv_serializer(ctx, value):
|
|
132
|
+
# Convert value to CSV bytes
|
|
133
|
+
return csv_bytes
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def register(func: _Serializer) -> None:
|
|
139
|
+
from schemathesis.transport.asgi import ASGI_TRANSPORT
|
|
140
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
|
141
|
+
from schemathesis.transport.wsgi import WSGI_TRANSPORT
|
|
142
|
+
|
|
143
|
+
@ASGI_TRANSPORT.serializer(*media_types)
|
|
144
|
+
@REQUESTS_TRANSPORT.serializer(*media_types)
|
|
145
|
+
@WSGI_TRANSPORT.serializer(*media_types)
|
|
146
|
+
def inner(ctx: SerializationContext, value: Any) -> dict[str, bytes]:
|
|
147
|
+
result = {}
|
|
148
|
+
serialized = func(ctx, value)
|
|
149
|
+
if serialized is not None:
|
|
150
|
+
result["data"] = serialized
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
return register
|
|
154
|
+
|
|
155
|
+
def alias(self, target: str | list[str], source: str) -> None:
|
|
156
|
+
"""Reuse an existing serializer for additional media types.
|
|
157
|
+
|
|
158
|
+
Register alias(es) for a built-in or previously registered serializer without
|
|
159
|
+
duplicating implementation.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
target: Media type(s) to register as aliases
|
|
163
|
+
source: Existing media type whose serializer to reuse
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If source media type has no registered serializer
|
|
167
|
+
ValueError: If target is empty
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
171
|
+
# Reuse built-in YAML serializer for custom media type
|
|
172
|
+
schemathesis.serializer.alias("application/custom+yaml", "application/yaml")
|
|
173
|
+
|
|
174
|
+
# Reuse built-in JSON serializer for vendor-specific type
|
|
175
|
+
schemathesis.serializer.alias("application/vnd.api+json", "application/json")
|
|
176
|
+
|
|
177
|
+
# Register multiple aliases at once
|
|
178
|
+
schemathesis.serializer.alias(
|
|
179
|
+
["application/x-json", "text/json"],
|
|
180
|
+
"application/json"
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
from schemathesis.transport.asgi import ASGI_TRANSPORT
|
|
186
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
|
187
|
+
from schemathesis.transport.wsgi import WSGI_TRANSPORT
|
|
188
|
+
|
|
189
|
+
if not source:
|
|
190
|
+
raise ValueError("Source media type cannot be empty")
|
|
191
|
+
|
|
192
|
+
targets = [target] if isinstance(target, str) else target
|
|
193
|
+
|
|
194
|
+
if not targets or any(not t for t in targets):
|
|
195
|
+
raise ValueError("Target media type cannot be empty")
|
|
196
|
+
|
|
197
|
+
# Get serializer from source (use requests transport as reference)
|
|
198
|
+
pair = REQUESTS_TRANSPORT.get_first_matching_media_type(source)
|
|
199
|
+
if pair is None:
|
|
200
|
+
raise ValueError(f"No serializer found for media type: {source}")
|
|
201
|
+
|
|
202
|
+
_, serializer_func = pair
|
|
203
|
+
|
|
204
|
+
# Register for all targets across all transports
|
|
205
|
+
for t in targets:
|
|
206
|
+
REQUESTS_TRANSPORT._serializers[t] = serializer_func
|
|
207
|
+
ASGI_TRANSPORT._serializers[t] = serializer_func
|
|
208
|
+
WSGI_TRANSPORT._serializers[t] = serializer_func
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
serializer = SerializerRegistry()
|
|
212
|
+
serializer.__doc__ = """Registry for serializers with decorator and aliasing support.
|
|
213
|
+
|
|
214
|
+
Use as a decorator to register custom serializers:
|
|
215
|
+
|
|
216
|
+
@schemathesis.serializer("text/csv")
|
|
217
|
+
def csv_serializer(ctx, value):
|
|
218
|
+
# Convert value to CSV bytes
|
|
219
|
+
return csv_bytes
|
|
220
|
+
|
|
221
|
+
Or use the alias method to reuse built-in serializers:
|
|
222
|
+
|
|
223
|
+
schemathesis.serializer.alias("application/custom+yaml", "application/yaml")
|
|
224
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from schemathesis.core.transport import Response
|
|
6
|
+
from schemathesis.generation.case import Case
|
|
7
|
+
from schemathesis.python import asgi
|
|
8
|
+
from schemathesis.transport.prepare import normalize_base_url
|
|
9
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT, RequestsTransport
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ASGITransport(RequestsTransport):
|
|
16
|
+
def send(self, case: Case, *, session: requests.Session | None = None, **kwargs: Any) -> Response:
|
|
17
|
+
if kwargs.get("base_url") is None:
|
|
18
|
+
kwargs["base_url"] = normalize_base_url(case.operation.base_url)
|
|
19
|
+
application = kwargs.pop("app", case.operation.app)
|
|
20
|
+
|
|
21
|
+
with asgi.get_client(application) as client:
|
|
22
|
+
return super().send(case, session=client, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ASGI_TRANSPORT = ASGITransport()
|
|
26
|
+
ASGI_TRANSPORT._copy_serializers_from(REQUESTS_TRANSPORT)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Mapping, cast
|
|
5
|
+
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
|
6
|
+
|
|
7
|
+
from schemathesis.config import SanitizationConfig
|
|
8
|
+
from schemathesis.core import SCHEMATHESIS_TEST_CASE_HEADER, NotSet
|
|
9
|
+
from schemathesis.core.errors import InvalidSchema
|
|
10
|
+
from schemathesis.core.output.sanitization import sanitize_url, sanitize_value
|
|
11
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
12
|
+
from schemathesis.core.transport import USER_AGENT
|
|
13
|
+
from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from requests import PreparedRequest
|
|
17
|
+
from requests.structures import CaseInsensitiveDict
|
|
18
|
+
|
|
19
|
+
from schemathesis.generation.case import Case
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache()
|
|
23
|
+
def get_default_headers() -> CaseInsensitiveDict:
|
|
24
|
+
from requests.utils import default_headers
|
|
25
|
+
|
|
26
|
+
headers = default_headers()
|
|
27
|
+
headers["User-Agent"] = USER_AGENT
|
|
28
|
+
return headers
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_headers(case: Case, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
|
|
32
|
+
default_headers = get_default_headers().copy()
|
|
33
|
+
if case.headers:
|
|
34
|
+
default_headers.update(case.headers)
|
|
35
|
+
default_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, case.id)
|
|
36
|
+
if headers:
|
|
37
|
+
default_headers.update(headers)
|
|
38
|
+
return default_headers
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_exclude_headers(case: Case) -> list[str]:
|
|
42
|
+
if (
|
|
43
|
+
case.meta is not None
|
|
44
|
+
and isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
45
|
+
and case.meta.phase.data.scenario == CoverageScenario.MISSING_PARAMETER
|
|
46
|
+
and case.meta.phase.data.parameter_location == ParameterLocation.HEADER
|
|
47
|
+
and case.meta.phase.data.parameter is not None
|
|
48
|
+
):
|
|
49
|
+
return [case.meta.phase.data.parameter]
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def prepare_url(case: Case, base_url: str | None) -> str:
|
|
54
|
+
"""Prepare URL based on case type."""
|
|
55
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
|
56
|
+
|
|
57
|
+
base_url = base_url or case.operation.base_url
|
|
58
|
+
assert base_url is not None
|
|
59
|
+
path = prepare_path(case.path, case.path_parameters)
|
|
60
|
+
|
|
61
|
+
if isinstance(case.operation.schema, GraphQLSchema):
|
|
62
|
+
parts = list(urlsplit(base_url))
|
|
63
|
+
parts[2] = path
|
|
64
|
+
return urlunsplit(parts)
|
|
65
|
+
else:
|
|
66
|
+
path = path.lstrip("/")
|
|
67
|
+
if not base_url.endswith("/"):
|
|
68
|
+
base_url += "/"
|
|
69
|
+
return unquote(urljoin(base_url, quote(path)))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def prepare_body(case: Case) -> list | dict[str, Any] | str | int | float | bool | bytes | NotSet:
|
|
73
|
+
"""Prepare body based on case type."""
|
|
74
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
|
75
|
+
|
|
76
|
+
if isinstance(case.operation.schema, GraphQLSchema):
|
|
77
|
+
return case.body if isinstance(case.body, (NotSet, bytes)) else {"query": case.body}
|
|
78
|
+
return case.body
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_base_url(base_url: str | None) -> str | None:
|
|
82
|
+
"""Normalize base URL by ensuring proper hostname for local URLs.
|
|
83
|
+
|
|
84
|
+
If URL has no hostname (typical for WSGI apps), adds "localhost" as default hostname.
|
|
85
|
+
"""
|
|
86
|
+
if base_url is None:
|
|
87
|
+
return None
|
|
88
|
+
parts = urlsplit(base_url)
|
|
89
|
+
if not parts.hostname:
|
|
90
|
+
path = cast(str, parts.path or "")
|
|
91
|
+
return urlunsplit(("http", "localhost", path or "", "", ""))
|
|
92
|
+
return base_url
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def prepare_path(path: str, parameters: dict[str, Any] | None) -> str:
|
|
96
|
+
try:
|
|
97
|
+
return path.format(**parameters or {})
|
|
98
|
+
except KeyError as exc:
|
|
99
|
+
# This may happen when a path template has a placeholder for variable "X", but parameter "X" is not defined
|
|
100
|
+
# in the parameters list.
|
|
101
|
+
# When `exc` is formatted, it is the missing key name in quotes. E.g. 'id'
|
|
102
|
+
raise InvalidSchema(f"Path parameter {exc} is not defined") from exc
|
|
103
|
+
except (IndexError, ValueError) as exc:
|
|
104
|
+
# A single unmatched `}` inside the path template may cause this
|
|
105
|
+
raise InvalidSchema(f"Malformed path template: `{path}`\n\n {exc}") from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def prepare_request(case: Case, headers: Mapping[str, Any] | None, *, config: SanitizationConfig) -> PreparedRequest:
|
|
109
|
+
import requests
|
|
110
|
+
|
|
111
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
|
112
|
+
|
|
113
|
+
base_url = normalize_base_url(case.operation.base_url)
|
|
114
|
+
kwargs = REQUESTS_TRANSPORT.serialize_case(case, base_url=base_url, headers=headers)
|
|
115
|
+
if config.enabled:
|
|
116
|
+
kwargs["url"] = sanitize_url(kwargs["url"], config=config)
|
|
117
|
+
kwargs["headers"] = dict(kwargs["headers"])
|
|
118
|
+
sanitize_value(kwargs["headers"], config=config)
|
|
119
|
+
if kwargs["cookies"]:
|
|
120
|
+
kwargs["cookies"] = dict(kwargs["cookies"])
|
|
121
|
+
sanitize_value(kwargs["cookies"], config=config)
|
|
122
|
+
if kwargs["params"]:
|
|
123
|
+
kwargs["params"] = dict(kwargs["params"])
|
|
124
|
+
sanitize_value(kwargs["params"], config=config)
|
|
125
|
+
|
|
126
|
+
return requests.Request(**kwargs).prepare()
|
|
@@ -0,0 +1,278 @@
|
|
|
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, MutableMapping
|
|
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 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 get_exclude_headers, 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 = dict(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
|
+
excluded_headers = get_exclude_headers(case)
|
|
84
|
+
for name in excluded_headers:
|
|
85
|
+
data["headers"].pop(name, None)
|
|
86
|
+
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
def send(self, case: Case, *, session: requests.Session | None = None, **kwargs: Any) -> Response:
|
|
90
|
+
import requests
|
|
91
|
+
|
|
92
|
+
config = case.operation.schema.config
|
|
93
|
+
|
|
94
|
+
max_redirects = kwargs.pop("max_redirects", None) or config.max_redirects_for(operation=case.operation)
|
|
95
|
+
timeout = config.request_timeout_for(operation=case.operation)
|
|
96
|
+
verify = config.tls_verify_for(operation=case.operation)
|
|
97
|
+
cert = config.request_cert_for(operation=case.operation)
|
|
98
|
+
|
|
99
|
+
if session is not None and session.headers:
|
|
100
|
+
# These headers are explicitly provided via config or CLI args.
|
|
101
|
+
# They have lower priority than ones provided via `kwargs`
|
|
102
|
+
headers = kwargs.setdefault("headers", {}) or {}
|
|
103
|
+
for name, value in session.headers.items():
|
|
104
|
+
headers.setdefault(name, value)
|
|
105
|
+
kwargs["headers"] = headers
|
|
106
|
+
|
|
107
|
+
data = self.serialize_case(case, **kwargs)
|
|
108
|
+
|
|
109
|
+
if verify is not None:
|
|
110
|
+
data.setdefault("verify", verify)
|
|
111
|
+
if timeout is not None:
|
|
112
|
+
data.setdefault("timeout", timeout)
|
|
113
|
+
if cert is not None:
|
|
114
|
+
data.setdefault("cert", cert)
|
|
115
|
+
|
|
116
|
+
kwargs.pop("base_url", None)
|
|
117
|
+
for key, value in kwargs.items():
|
|
118
|
+
if key not in ("headers", "cookies", "params") or key not in data:
|
|
119
|
+
data[key] = value
|
|
120
|
+
data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT)
|
|
121
|
+
|
|
122
|
+
current_session_headers: MutableMapping[str, Any] = {}
|
|
123
|
+
current_session_auth = None
|
|
124
|
+
|
|
125
|
+
if session is None:
|
|
126
|
+
validate_vanilla_requests_kwargs(data)
|
|
127
|
+
session = requests.Session()
|
|
128
|
+
close_session = True
|
|
129
|
+
else:
|
|
130
|
+
current_session_headers = session.headers
|
|
131
|
+
if isinstance(session.auth, tuple):
|
|
132
|
+
excluded_headers = get_exclude_headers(case)
|
|
133
|
+
if "Authorization" in excluded_headers:
|
|
134
|
+
current_session_auth = session.auth
|
|
135
|
+
session.auth = None
|
|
136
|
+
close_session = False
|
|
137
|
+
if max_redirects is not None:
|
|
138
|
+
session.max_redirects = max_redirects
|
|
139
|
+
session.headers = {}
|
|
140
|
+
|
|
141
|
+
verify = data.get("verify", True)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
rate_limit = config.rate_limit_for(operation=case.operation)
|
|
145
|
+
with ratelimit(rate_limit, config.base_url):
|
|
146
|
+
response = session.request(**data)
|
|
147
|
+
return Response.from_requests(
|
|
148
|
+
response,
|
|
149
|
+
verify=verify,
|
|
150
|
+
_override=Override(
|
|
151
|
+
query=kwargs.get("params") or {},
|
|
152
|
+
headers=kwargs.get("headers") or {},
|
|
153
|
+
cookies=kwargs.get("cookies") or {},
|
|
154
|
+
path_parameters={},
|
|
155
|
+
body={},
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
finally:
|
|
159
|
+
session.headers = current_session_headers
|
|
160
|
+
if current_session_auth is not None:
|
|
161
|
+
session.auth = current_session_auth
|
|
162
|
+
if close_session:
|
|
163
|
+
session.close()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
|
|
167
|
+
"""Check arguments for `requests.Session.request`.
|
|
168
|
+
|
|
169
|
+
Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
|
|
170
|
+
`requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
|
|
171
|
+
"""
|
|
172
|
+
url = data["url"]
|
|
173
|
+
if not urlparse(url).netloc:
|
|
174
|
+
stack = inspect.stack()
|
|
175
|
+
method_name = "call"
|
|
176
|
+
for frame in stack[1:]:
|
|
177
|
+
if frame.function == "call_and_validate":
|
|
178
|
+
method_name = "call_and_validate"
|
|
179
|
+
break
|
|
180
|
+
raise RuntimeError(
|
|
181
|
+
"The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
|
|
182
|
+
f"Pass `base_url` either to the `schemathesis.openapi.from_*` loader or to the `Case.{method_name}`.\n"
|
|
183
|
+
f"If you use the ASGI integration, please supply your test client "
|
|
184
|
+
f"as the `session` argument to `call`.\nURL: {url}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
REQUESTS_TRANSPORT = RequestsTransport()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@REQUESTS_TRANSPORT.serializer("application/json", "text/json")
|
|
192
|
+
def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
193
|
+
return serialize_json(value)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@REQUESTS_TRANSPORT.serializer(
|
|
197
|
+
"text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
|
|
198
|
+
)
|
|
199
|
+
def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
200
|
+
return serialize_yaml(value)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _should_coerce_to_bytes(item: Any) -> bool:
|
|
204
|
+
"""Whether the item should be converted to bytes."""
|
|
205
|
+
# These types are OK in forms, others should be coerced to bytes
|
|
206
|
+
return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
|
|
210
|
+
"""Make the generated data suitable for sending as multipart.
|
|
211
|
+
|
|
212
|
+
If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
|
|
213
|
+
we convert it to bytes and send it as-is, ignoring any conversion errors.
|
|
214
|
+
|
|
215
|
+
NOTE. This behavior might change in the future.
|
|
216
|
+
"""
|
|
217
|
+
for name, value in data.items():
|
|
218
|
+
if isinstance(value, list):
|
|
219
|
+
data[name] = [serialize_binary(item) if _should_coerce_to_bytes(item) else item for item in value]
|
|
220
|
+
elif _should_coerce_to_bytes(value):
|
|
221
|
+
data[name] = serialize_binary(value)
|
|
222
|
+
return data
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def choose_boundary() -> str:
|
|
226
|
+
"""Random boundary name."""
|
|
227
|
+
return binascii.hexlify(os.urandom(16)).decode("ascii")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _encode_multipart(value: Any, boundary: str) -> bytes:
|
|
231
|
+
"""Encode any value as multipart.
|
|
232
|
+
|
|
233
|
+
NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
|
|
234
|
+
be used as multipart, in cases when the API schema dictates so.
|
|
235
|
+
"""
|
|
236
|
+
# For such cases we stringify the value and wrap it to a randomly-generated boundary
|
|
237
|
+
body = BytesIO()
|
|
238
|
+
body.write(f"--{boundary}\r\n".encode())
|
|
239
|
+
body.write(str(value).encode())
|
|
240
|
+
body.write(f"--{boundary}--\r\n".encode("latin-1"))
|
|
241
|
+
return body.getvalue()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@REQUESTS_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
|
|
245
|
+
def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
246
|
+
if isinstance(value, bytes):
|
|
247
|
+
return {"data": value}
|
|
248
|
+
if isinstance(value, dict):
|
|
249
|
+
multipart = _prepare_form_data(value)
|
|
250
|
+
files, data = ctx.case.operation.prepare_multipart(multipart)
|
|
251
|
+
return {"files": files, "data": data}
|
|
252
|
+
# Uncommon schema. For example - `{"type": "string"}`
|
|
253
|
+
boundary = choose_boundary()
|
|
254
|
+
raw_data = _encode_multipart(value, boundary)
|
|
255
|
+
content_type = f"multipart/form-data; boundary={boundary}"
|
|
256
|
+
return {"data": raw_data, "headers": {"Content-Type": content_type}}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@REQUESTS_TRANSPORT.serializer("application/xml", "text/xml")
|
|
260
|
+
def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
261
|
+
return serialize_xml(ctx.case, value)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@REQUESTS_TRANSPORT.serializer("application/x-www-form-urlencoded")
|
|
265
|
+
def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
266
|
+
return {"data": value}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@REQUESTS_TRANSPORT.serializer("text/plain")
|
|
270
|
+
def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
271
|
+
if isinstance(value, bytes):
|
|
272
|
+
return {"data": value}
|
|
273
|
+
return {"data": str(value).encode("utf8")}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@REQUESTS_TRANSPORT.serializer("application/octet-stream")
|
|
277
|
+
def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
278
|
+
return {"data": serialize_binary(value)}
|