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,329 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Union
|
|
7
|
+
from unicodedata import normalize
|
|
8
|
+
|
|
9
|
+
from schemathesis.core.errors import UnboundPrefix
|
|
10
|
+
from schemathesis.core.transforms import transform
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from schemathesis.core.compat import RefResolver
|
|
14
|
+
from schemathesis.generation.case import Case
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Binary(str):
|
|
19
|
+
"""A wrapper around `bytes` to resolve OpenAPI and JSON Schema `format` discrepancies.
|
|
20
|
+
|
|
21
|
+
Treat `bytes` as a valid type, allowing generation of bytes for OpenAPI `format` values like `binary` or `file`
|
|
22
|
+
that JSON Schema expects to be strings.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
data: bytes
|
|
26
|
+
|
|
27
|
+
__slots__ = ("data",)
|
|
28
|
+
|
|
29
|
+
def __hash__(self) -> int:
|
|
30
|
+
return hash(self.data)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def serialize_json(value: Any) -> dict[str, Any]:
|
|
34
|
+
if isinstance(value, bytes):
|
|
35
|
+
# Possible to get via explicit examples, e.g. `externalValue`
|
|
36
|
+
return {"data": value}
|
|
37
|
+
if isinstance(value, Binary):
|
|
38
|
+
return {"data": value.data}
|
|
39
|
+
if value is None:
|
|
40
|
+
# If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
|
|
41
|
+
# argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
|
|
42
|
+
# Therefore we explicitly create such payload
|
|
43
|
+
return {"data": b"null"}
|
|
44
|
+
return {"json": value}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _replace_binary(value: dict) -> dict:
|
|
48
|
+
return {key: value.data if isinstance(value, Binary) else value for key, value in value.items()}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def serialize_binary(value: Any) -> bytes:
|
|
52
|
+
"""Convert the input value to bytes and ignore any conversion errors."""
|
|
53
|
+
if isinstance(value, bytes):
|
|
54
|
+
return value
|
|
55
|
+
if isinstance(value, Binary):
|
|
56
|
+
return value.data
|
|
57
|
+
return str(value).encode(errors="ignore")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def serialize_yaml(value: Any) -> dict[str, Any]:
|
|
61
|
+
import yaml
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
from yaml import CSafeDumper as SafeDumper
|
|
65
|
+
except ImportError:
|
|
66
|
+
from yaml import SafeDumper # type: ignore[assignment]
|
|
67
|
+
|
|
68
|
+
if isinstance(value, bytes):
|
|
69
|
+
return {"data": value}
|
|
70
|
+
if isinstance(value, Binary):
|
|
71
|
+
return {"data": value.data}
|
|
72
|
+
if isinstance(value, (list, dict)):
|
|
73
|
+
value = transform(value, _replace_binary)
|
|
74
|
+
return {"data": yaml.dump(value, Dumper=SafeDumper)}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Primitive = Union[str, int, float, bool, None]
|
|
78
|
+
JSON = Union[Primitive, List, Dict[str, Any]]
|
|
79
|
+
DEFAULT_TAG_NAME = "data"
|
|
80
|
+
NAMESPACE_URL = "http://example.com/schema"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def serialize_xml(case: Case, value: Any) -> dict[str, Any]:
|
|
84
|
+
media_type = case.media_type
|
|
85
|
+
|
|
86
|
+
assert media_type is not None
|
|
87
|
+
|
|
88
|
+
schema = None
|
|
89
|
+
resource_name = None
|
|
90
|
+
|
|
91
|
+
for body in case.operation.get_bodies_for_media_type(media_type):
|
|
92
|
+
schema = body.optimized_schema
|
|
93
|
+
resource_name = body.resource_name
|
|
94
|
+
break
|
|
95
|
+
assert schema is not None, (case.operation.body, media_type)
|
|
96
|
+
|
|
97
|
+
return _serialize_xml(value, schema, resource_name=resource_name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _serialize_xml(value: Any, schema: dict[str, Any], resource_name: str | None) -> dict[str, Any]:
|
|
101
|
+
"""Serialize a generated Python object as an XML string.
|
|
102
|
+
|
|
103
|
+
Schemas may contain additional information for fine-tuned XML serialization.
|
|
104
|
+
"""
|
|
105
|
+
from schemathesis.core.compat import RefResolver
|
|
106
|
+
|
|
107
|
+
if isinstance(value, (bytes, str)):
|
|
108
|
+
return {"data": value}
|
|
109
|
+
resolver = RefResolver.from_schema(schema)
|
|
110
|
+
if "$ref" in schema:
|
|
111
|
+
_, schema = resolver.resolve(schema["$ref"])
|
|
112
|
+
tag = _get_xml_tag(schema, resource_name)
|
|
113
|
+
buffer = StringIO()
|
|
114
|
+
# Collect all namespaces to ensure that all child nodes with prefixes have proper namespaces in their parent nodes
|
|
115
|
+
namespace_stack: list[str] = []
|
|
116
|
+
_write_xml(buffer, value, tag, schema, namespace_stack, resolver)
|
|
117
|
+
data = buffer.getvalue()
|
|
118
|
+
return {"data": data.encode("utf8")}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_xml_tag(schema: dict[str, Any] | None, resource_name: str | None) -> str:
|
|
122
|
+
# On the top level we need to detect the proper XML tag, in other cases it is known from object properties
|
|
123
|
+
if (schema or {}).get("xml", {}).get("name"):
|
|
124
|
+
return (schema or {})["xml"]["name"]
|
|
125
|
+
if resource_name is not None:
|
|
126
|
+
return resource_name
|
|
127
|
+
|
|
128
|
+
# Here we don't have any name for the payload schema - no reference or the `xml` property
|
|
129
|
+
return DEFAULT_TAG_NAME
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _write_xml(
|
|
133
|
+
buffer: StringIO,
|
|
134
|
+
value: JSON,
|
|
135
|
+
tag: str,
|
|
136
|
+
schema: dict[str, Any] | None,
|
|
137
|
+
namespace_stack: list[str],
|
|
138
|
+
resolver: RefResolver,
|
|
139
|
+
) -> None:
|
|
140
|
+
if isinstance(value, dict):
|
|
141
|
+
_write_object(buffer, value, tag, schema, namespace_stack, resolver)
|
|
142
|
+
elif isinstance(value, list):
|
|
143
|
+
_write_array(buffer, value, tag, schema, namespace_stack, resolver)
|
|
144
|
+
else:
|
|
145
|
+
_write_primitive(buffer, value, tag, schema, namespace_stack)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_prefix(options: dict[str, Any], namespace_stack: list[str]) -> None:
|
|
149
|
+
try:
|
|
150
|
+
prefix = options["prefix"]
|
|
151
|
+
if prefix not in namespace_stack:
|
|
152
|
+
raise UnboundPrefix(prefix)
|
|
153
|
+
except KeyError:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def push_namespace_if_any(namespace_stack: list[str], options: dict[str, Any]) -> None:
|
|
158
|
+
if "namespace" in options and "prefix" in options:
|
|
159
|
+
namespace_stack.append(options["prefix"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def pop_namespace_if_any(namespace_stack: list[str], options: dict[str, Any]) -> None:
|
|
163
|
+
if "namespace" in options and "prefix" in options:
|
|
164
|
+
namespace_stack.pop()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _write_object(
|
|
168
|
+
buffer: StringIO,
|
|
169
|
+
obj: dict[str, JSON],
|
|
170
|
+
tag: str,
|
|
171
|
+
schema: dict[str, Any] | None,
|
|
172
|
+
stack: list[str],
|
|
173
|
+
resolver: RefResolver,
|
|
174
|
+
) -> None:
|
|
175
|
+
options = (schema or {}).get("xml", {})
|
|
176
|
+
push_namespace_if_any(stack, options)
|
|
177
|
+
tag = _sanitize_xml_name(tag)
|
|
178
|
+
if "prefix" in options:
|
|
179
|
+
tag = f"{options['prefix']}:{tag}"
|
|
180
|
+
buffer.write(f"<{tag}")
|
|
181
|
+
if "namespace" in options:
|
|
182
|
+
_write_namespace(buffer, options)
|
|
183
|
+
|
|
184
|
+
attribute_namespaces = {}
|
|
185
|
+
attributes = {}
|
|
186
|
+
children_buffer = StringIO()
|
|
187
|
+
properties = (schema or {}).get("properties", {})
|
|
188
|
+
for child_name, value in obj.items():
|
|
189
|
+
property_schema = properties.get(child_name, {})
|
|
190
|
+
if "$ref" in property_schema:
|
|
191
|
+
_, property_schema = resolver.resolve(property_schema["$ref"])
|
|
192
|
+
child_options = property_schema.get("xml", {})
|
|
193
|
+
push_namespace_if_any(stack, child_options)
|
|
194
|
+
child_tag = child_options.get("name", child_name)
|
|
195
|
+
|
|
196
|
+
if child_options.get("attribute", False):
|
|
197
|
+
if child_options.get("prefix") and child_options.get("namespace"):
|
|
198
|
+
_validate_prefix(child_options, stack)
|
|
199
|
+
prefix = child_options["prefix"]
|
|
200
|
+
attr_name = f"{prefix}:{_sanitize_xml_name(child_tag)}"
|
|
201
|
+
# Store namespace declaration
|
|
202
|
+
attribute_namespaces[prefix] = child_options["namespace"]
|
|
203
|
+
else:
|
|
204
|
+
attr_name = _sanitize_xml_name(child_tag)
|
|
205
|
+
|
|
206
|
+
if attr_name not in attributes: # Only keep first occurrence
|
|
207
|
+
attributes[attr_name] = f'{attr_name}="{_escape_xml(value)}"'
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
child_tag = _sanitize_xml_name(child_tag)
|
|
211
|
+
if child_options.get("prefix"):
|
|
212
|
+
_validate_prefix(child_options, stack)
|
|
213
|
+
prefix = child_options["prefix"]
|
|
214
|
+
child_tag = f"{prefix}:{child_tag}"
|
|
215
|
+
_write_xml(children_buffer, value, child_tag, property_schema, stack, resolver)
|
|
216
|
+
pop_namespace_if_any(stack, child_options)
|
|
217
|
+
|
|
218
|
+
# Write namespace declarations for attributes
|
|
219
|
+
for prefix, namespace in attribute_namespaces.items():
|
|
220
|
+
buffer.write(f' xmlns:{prefix}="{namespace}"')
|
|
221
|
+
|
|
222
|
+
if attributes:
|
|
223
|
+
buffer.write(f" {' '.join(attributes.values())}")
|
|
224
|
+
buffer.write(">")
|
|
225
|
+
buffer.write(children_buffer.getvalue())
|
|
226
|
+
buffer.write(f"</{tag}>")
|
|
227
|
+
pop_namespace_if_any(stack, options)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _write_array(
|
|
231
|
+
buffer: StringIO, obj: list[JSON], tag: str, schema: dict[str, Any] | None, stack: list[str], resolver: RefResolver
|
|
232
|
+
) -> None:
|
|
233
|
+
options = (schema or {}).get("xml", {})
|
|
234
|
+
push_namespace_if_any(stack, options)
|
|
235
|
+
if options.get("prefix"):
|
|
236
|
+
tag = f"{options['prefix']}:{tag}"
|
|
237
|
+
wrapped = options.get("wrapped", False)
|
|
238
|
+
is_namespace_specified = False
|
|
239
|
+
if wrapped:
|
|
240
|
+
buffer.write(f"<{tag}")
|
|
241
|
+
if "namespace" in options:
|
|
242
|
+
is_namespace_specified = True
|
|
243
|
+
_write_namespace(buffer, options)
|
|
244
|
+
buffer.write(">")
|
|
245
|
+
# In Open API `items` value should be an object and not an array
|
|
246
|
+
if schema:
|
|
247
|
+
items = dict(schema.get("items", {}))
|
|
248
|
+
else:
|
|
249
|
+
items = {}
|
|
250
|
+
if "$ref" in items:
|
|
251
|
+
_, items = resolver.resolve(items["$ref"])
|
|
252
|
+
child_options = items.get("xml", {})
|
|
253
|
+
child_tag = child_options.get("name", tag)
|
|
254
|
+
if not is_namespace_specified and "namespace" in options:
|
|
255
|
+
child_options.setdefault("namespace", options["namespace"])
|
|
256
|
+
if "prefix" in options:
|
|
257
|
+
child_options.setdefault("prefix", options["prefix"])
|
|
258
|
+
items["xml"] = child_options
|
|
259
|
+
_validate_prefix(child_options, stack)
|
|
260
|
+
for item in obj:
|
|
261
|
+
_write_xml(buffer, item, child_tag, items, stack, resolver)
|
|
262
|
+
if wrapped:
|
|
263
|
+
buffer.write(f"</{tag}>")
|
|
264
|
+
pop_namespace_if_any(stack, options)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _write_primitive(
|
|
268
|
+
buffer: StringIO, obj: Primitive, tag: str, schema: dict[str, Any] | None, namespace_stack: list[str]
|
|
269
|
+
) -> None:
|
|
270
|
+
xml_options = (schema or {}).get("xml", {})
|
|
271
|
+
# There is no need for modifying the namespace stack, as we know that this function is terminal - it do not recurse
|
|
272
|
+
# and this element don't have any children. Therefore, checking the prefix is enough
|
|
273
|
+
_validate_prefix(xml_options, namespace_stack)
|
|
274
|
+
buffer.write(f"<{tag}")
|
|
275
|
+
if "namespace" in xml_options:
|
|
276
|
+
_write_namespace(buffer, xml_options)
|
|
277
|
+
buffer.write(f">{_escape_xml(obj)}</{tag}>")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _write_namespace(buffer: StringIO, options: dict[str, Any]) -> None:
|
|
281
|
+
buffer.write(" xmlns")
|
|
282
|
+
if "prefix" in options:
|
|
283
|
+
buffer.write(f":{options['prefix']}")
|
|
284
|
+
buffer.write(f'="{options["namespace"]}"')
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _escape_xml(value: JSON) -> str:
|
|
288
|
+
"""Escape special characters in XML content."""
|
|
289
|
+
if isinstance(value, (int, float, bool)):
|
|
290
|
+
return str(value)
|
|
291
|
+
if value is None:
|
|
292
|
+
return ""
|
|
293
|
+
|
|
294
|
+
# Filter out invalid XML characters
|
|
295
|
+
cleaned = "".join(
|
|
296
|
+
char
|
|
297
|
+
for char in str(value)
|
|
298
|
+
if (
|
|
299
|
+
char in "\t\n\r"
|
|
300
|
+
or 0x20 <= ord(char) <= 0xD7FF
|
|
301
|
+
or 0xE000 <= ord(char) <= 0xFFFD
|
|
302
|
+
or 0x10000 <= ord(char) <= 0x10FFFF
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
replacements = {
|
|
307
|
+
"&": "&",
|
|
308
|
+
"<": "<",
|
|
309
|
+
">": ">",
|
|
310
|
+
'"': """,
|
|
311
|
+
"'": "'",
|
|
312
|
+
}
|
|
313
|
+
return "".join(replacements.get(c, c) for c in cleaned)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _sanitize_xml_name(name: str) -> str:
|
|
317
|
+
"""Sanitize a string to be a valid XML element name."""
|
|
318
|
+
if not name:
|
|
319
|
+
return "element"
|
|
320
|
+
|
|
321
|
+
name = normalize("NFKC", str(name))
|
|
322
|
+
|
|
323
|
+
name = name.replace(":", "_")
|
|
324
|
+
sanitized = re.sub(r"[^a-zA-Z0-9_\-.]", "_", name)
|
|
325
|
+
|
|
326
|
+
if not sanitized[0].isalpha() and sanitized[0] != "_":
|
|
327
|
+
sanitized = "x_" + sanitized
|
|
328
|
+
|
|
329
|
+
return sanitized
|
|
@@ -0,0 +1,175 @@
|
|
|
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 (
|
|
16
|
+
get_exclude_headers,
|
|
17
|
+
normalize_base_url,
|
|
18
|
+
prepare_body,
|
|
19
|
+
prepare_headers,
|
|
20
|
+
prepare_path,
|
|
21
|
+
)
|
|
22
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
|
23
|
+
from schemathesis.transport.serialization import serialize_binary, serialize_json, serialize_xml, serialize_yaml
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import werkzeug
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WSGITransport(BaseTransport["werkzeug.Client"]):
|
|
30
|
+
def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
|
|
31
|
+
headers = kwargs.get("headers")
|
|
32
|
+
params = kwargs.get("params")
|
|
33
|
+
|
|
34
|
+
final_headers = prepare_headers(case, headers)
|
|
35
|
+
|
|
36
|
+
media_type = case.media_type
|
|
37
|
+
|
|
38
|
+
# Set content type for payload
|
|
39
|
+
if media_type and not isinstance(case.body, NotSet):
|
|
40
|
+
final_headers["Content-Type"] = media_type
|
|
41
|
+
|
|
42
|
+
extra: dict[str, Any]
|
|
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
|
+
data = {
|
|
52
|
+
"method": case.method,
|
|
53
|
+
"path": case.operation.schema.get_full_path(prepare_path(case.path, case.path_parameters)),
|
|
54
|
+
# Convert to regular dict for Werkzeug compatibility
|
|
55
|
+
"headers": dict(final_headers),
|
|
56
|
+
"query_string": case.query,
|
|
57
|
+
**extra,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if params is not None:
|
|
61
|
+
merge_at(data, "query_string", params)
|
|
62
|
+
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
def send(
|
|
66
|
+
self,
|
|
67
|
+
case: Case,
|
|
68
|
+
*,
|
|
69
|
+
session: werkzeug.Client | None = None,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> Response:
|
|
72
|
+
import requests
|
|
73
|
+
|
|
74
|
+
headers = kwargs.pop("headers", None)
|
|
75
|
+
params = kwargs.pop("params", None)
|
|
76
|
+
cookies = kwargs.pop("cookies", None)
|
|
77
|
+
application = kwargs.pop("app")
|
|
78
|
+
|
|
79
|
+
data = self.serialize_case(case, headers=headers, params=params)
|
|
80
|
+
data.update({key: value for key, value in kwargs.items() if key not in data})
|
|
81
|
+
|
|
82
|
+
excluded_headers = get_exclude_headers(case)
|
|
83
|
+
for name in excluded_headers:
|
|
84
|
+
data["headers"].pop(name, None)
|
|
85
|
+
|
|
86
|
+
client = session or wsgi.get_client(application)
|
|
87
|
+
cookies = {**(case.cookies or {}), **(cookies or {})}
|
|
88
|
+
|
|
89
|
+
config = case.operation.schema.config
|
|
90
|
+
rate_limit = config.rate_limit_for(operation=case.operation)
|
|
91
|
+
with cookie_handler(client, cookies), ratelimit(rate_limit, config.base_url):
|
|
92
|
+
start = time.monotonic()
|
|
93
|
+
response = client.open(**data)
|
|
94
|
+
elapsed = time.monotonic() - start
|
|
95
|
+
|
|
96
|
+
requests_kwargs = REQUESTS_TRANSPORT.serialize_case(
|
|
97
|
+
case,
|
|
98
|
+
base_url=normalize_base_url(case.operation.base_url),
|
|
99
|
+
headers=headers,
|
|
100
|
+
params=params,
|
|
101
|
+
cookies=cookies,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
headers = {key: response.headers.getlist(key) for key in response.headers.keys()}
|
|
105
|
+
|
|
106
|
+
return Response(
|
|
107
|
+
status_code=response.status_code,
|
|
108
|
+
headers=headers,
|
|
109
|
+
content=response.get_data(),
|
|
110
|
+
request=requests.Request(**requests_kwargs).prepare(),
|
|
111
|
+
elapsed=elapsed,
|
|
112
|
+
verify=False,
|
|
113
|
+
_override=Override(
|
|
114
|
+
query=kwargs.get("params") or {},
|
|
115
|
+
headers=kwargs.get("headers") or {},
|
|
116
|
+
cookies=kwargs.get("cookies") or {},
|
|
117
|
+
path_parameters={},
|
|
118
|
+
body={},
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def cookie_handler(client: werkzeug.Client, cookies: dict[str, Any] | None) -> Generator[None, None, None]:
|
|
125
|
+
"""Set cookies required for a call."""
|
|
126
|
+
if not cookies:
|
|
127
|
+
yield
|
|
128
|
+
else:
|
|
129
|
+
for key, value in cookies.items():
|
|
130
|
+
client.set_cookie(key=key, value=value, domain="localhost")
|
|
131
|
+
yield
|
|
132
|
+
for key in cookies:
|
|
133
|
+
client.delete_cookie(key=key, domain="localhost")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
WSGI_TRANSPORT = WSGITransport()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@WSGI_TRANSPORT.serializer("application/json", "text/json")
|
|
140
|
+
def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
141
|
+
return serialize_json(value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@WSGI_TRANSPORT.serializer(
|
|
145
|
+
"text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
|
|
146
|
+
)
|
|
147
|
+
def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
148
|
+
return serialize_yaml(value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@WSGI_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
|
|
152
|
+
def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
153
|
+
return {"data": value}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@WSGI_TRANSPORT.serializer("application/xml", "text/xml")
|
|
157
|
+
def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
158
|
+
return serialize_xml(ctx.case, value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@WSGI_TRANSPORT.serializer("application/x-www-form-urlencoded")
|
|
162
|
+
def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
163
|
+
return {"data": value}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@WSGI_TRANSPORT.serializer("text/plain")
|
|
167
|
+
def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
168
|
+
if isinstance(value, bytes):
|
|
169
|
+
return {"data": value}
|
|
170
|
+
return {"data": str(value)}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@WSGI_TRANSPORT.serializer("application/octet-stream")
|
|
174
|
+
def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
|
175
|
+
return {"data": serialize_binary(value)}
|