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.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /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
+ "&": "&amp;",
308
+ "<": "&lt;",
309
+ ">": "&gt;",
310
+ '"': "&quot;",
311
+ "'": "&apos;",
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)}