schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl

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