schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,221 @@
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.transport import BaseTransport, SerializationContext
15
+ from schemathesis.transport.prepare import prepare_body, prepare_headers, prepare_url
16
+ from schemathesis.transport.serialization import Binary, serialize_binary, serialize_json, serialize_xml, serialize_yaml
17
+
18
+ if TYPE_CHECKING:
19
+ import requests
20
+
21
+ from schemathesis.generation.case import Case
22
+
23
+
24
+ class RequestsTransport(BaseTransport["Case", Response, "requests.Session"]):
25
+ def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
26
+ base_url = kwargs.get("base_url")
27
+ headers = kwargs.get("headers")
28
+ params = kwargs.get("params")
29
+ cookies = kwargs.get("cookies")
30
+
31
+ final_headers = prepare_headers(case, headers)
32
+
33
+ media_type = case.media_type
34
+
35
+ # Set content type header if needed
36
+ if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
37
+ if "content-type" not in final_headers:
38
+ final_headers["Content-Type"] = media_type
39
+
40
+ url = prepare_url(case, base_url)
41
+
42
+ # Handle serialization
43
+ if not isinstance(case.body, NotSet) and media_type is not None:
44
+ serializer = self._get_serializer(media_type)
45
+ context = SerializationContext(case=case)
46
+ extra = serializer(context, prepare_body(case))
47
+ else:
48
+ extra = {}
49
+
50
+ if case._auth is not None:
51
+ extra["auth"] = case._auth
52
+
53
+ # Additional headers from serializer
54
+ additional_headers = extra.pop("headers", None)
55
+ if additional_headers:
56
+ for key, value in additional_headers.items():
57
+ final_headers.setdefault(key, value)
58
+
59
+ data = {
60
+ "method": case.method,
61
+ "url": url,
62
+ "cookies": case.cookies,
63
+ "headers": final_headers,
64
+ "params": case.query,
65
+ **extra,
66
+ }
67
+
68
+ if params is not None:
69
+ merge_at(data, "params", params)
70
+ if cookies is not None:
71
+ merge_at(data, "cookies", cookies)
72
+
73
+ return data
74
+
75
+ def send(self, case: Case, *, session: requests.Session | None = None, **kwargs: Any) -> Response:
76
+ import requests
77
+
78
+ data = self.serialize_case(case, **kwargs)
79
+ kwargs.pop("base_url", None)
80
+ data.update({key: value for key, value in kwargs.items() if key not in data})
81
+ data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT)
82
+
83
+ if session is None:
84
+ validate_vanilla_requests_kwargs(data)
85
+ session = requests.Session()
86
+ close_session = True
87
+ else:
88
+ close_session = False
89
+
90
+ verify = data.get("verify", True)
91
+
92
+ try:
93
+ with ratelimit(case.operation.schema.rate_limiter, case.operation.schema.base_url):
94
+ response = session.request(**data) # type: ignore
95
+ return Response.from_requests(response, verify=verify)
96
+ finally:
97
+ if close_session:
98
+ session.close()
99
+
100
+
101
+ def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
102
+ """Check arguments for `requests.Session.request`.
103
+
104
+ Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
105
+ `requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
106
+ """
107
+ url = data["url"]
108
+ if not urlparse(url).netloc:
109
+ stack = inspect.stack()
110
+ method_name = "call"
111
+ for frame in stack[1:]:
112
+ if frame.function == "call_and_validate":
113
+ method_name = "call_and_validate"
114
+ break
115
+ raise RuntimeError(
116
+ "The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
117
+ f"Pass `base_url` either to the `schemathesis.openapi.from_*` loader or to the `Case.{method_name}`.\n"
118
+ f"If you use the ASGI integration, please supply your test client "
119
+ f"as the `session` argument to `call`.\nURL: {url}"
120
+ )
121
+
122
+
123
+ REQUESTS_TRANSPORT = RequestsTransport()
124
+
125
+
126
+ @REQUESTS_TRANSPORT.serializer("application/json", "text/json")
127
+ def json_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
128
+ return serialize_json(value)
129
+
130
+
131
+ @REQUESTS_TRANSPORT.serializer(
132
+ "text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
133
+ )
134
+ def yaml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
135
+ return serialize_yaml(value)
136
+
137
+
138
+ def _should_coerce_to_bytes(item: Any) -> bool:
139
+ """Whether the item should be converted to bytes."""
140
+ # These types are OK in forms, others should be coerced to bytes
141
+ return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
142
+
143
+
144
+ def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
145
+ """Make the generated data suitable for sending as multipart.
146
+
147
+ If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
148
+ we convert it to bytes and send it as-is, ignoring any conversion errors.
149
+
150
+ NOTE. This behavior might change in the future.
151
+ """
152
+ for name, value in data.items():
153
+ if isinstance(value, list):
154
+ data[name] = [serialize_binary(item) if _should_coerce_to_bytes(item) else item for item in value]
155
+ elif _should_coerce_to_bytes(value):
156
+ data[name] = serialize_binary(value)
157
+ return data
158
+
159
+
160
+ def choose_boundary() -> str:
161
+ """Random boundary name."""
162
+ return binascii.hexlify(os.urandom(16)).decode("ascii")
163
+
164
+
165
+ def _encode_multipart(value: Any, boundary: str) -> bytes:
166
+ """Encode any value as multipart.
167
+
168
+ NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
169
+ be used as multipart, in cases when the API schema dictates so.
170
+ """
171
+ # For such cases we stringify the value and wrap it to a randomly-generated boundary
172
+ body = BytesIO()
173
+ body.write(f"--{boundary}\r\n".encode())
174
+ body.write(str(value).encode())
175
+ body.write(f"--{boundary}--\r\n".encode("latin-1"))
176
+ return body.getvalue()
177
+
178
+
179
+ @REQUESTS_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
180
+ def multipart_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
181
+ if isinstance(value, bytes):
182
+ return {"data": value}
183
+ if isinstance(value, dict):
184
+ value = deepclone(value)
185
+ multipart = _prepare_form_data(value)
186
+ files, data = ctx.case.operation.prepare_multipart(multipart)
187
+ return {"files": files, "data": data}
188
+ # Uncommon schema. For example - `{"type": "string"}`
189
+ boundary = choose_boundary()
190
+ raw_data = _encode_multipart(value, boundary)
191
+ content_type = f"multipart/form-data; boundary={boundary}"
192
+ return {"data": raw_data, "headers": {"Content-Type": content_type}}
193
+
194
+
195
+ @REQUESTS_TRANSPORT.serializer("application/xml", "text/xml")
196
+ def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
197
+ media_type = ctx.case.media_type
198
+
199
+ assert media_type is not None
200
+
201
+ raw_schema = ctx.case.operation.get_raw_payload_schema(media_type)
202
+ resolved_schema = ctx.case.operation.get_resolved_payload_schema(media_type)
203
+
204
+ return serialize_xml(value, raw_schema, resolved_schema)
205
+
206
+
207
+ @REQUESTS_TRANSPORT.serializer("application/x-www-form-urlencoded")
208
+ def urlencoded_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
209
+ return {"data": value}
210
+
211
+
212
+ @REQUESTS_TRANSPORT.serializer("text/plain")
213
+ def text_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
214
+ if isinstance(value, bytes):
215
+ return {"data": value}
216
+ return {"data": str(value).encode("utf8")}
217
+
218
+
219
+ @REQUESTS_TRANSPORT.serializer("application/octet-stream")
220
+ def binary_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
221
+ 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,7 +76,9 @@ 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.
@@ -64,7 +126,7 @@ def _validate_prefix(options: dict[str, Any], namespace_stack: list[str]) -> Non
64
126
  try:
65
127
  prefix = options["prefix"]
66
128
  if prefix not in namespace_stack:
67
- raise UnboundPrefixError(prefix)
129
+ raise UnboundPrefix(prefix)
68
130
  except KeyError:
69
131
  pass
70
132
 
@@ -149,7 +211,7 @@ def _write_array(buffer: StringIO, obj: list[JSON], tag: str, schema: dict[str,
149
211
  _write_namespace(buffer, options)
150
212
  buffer.write(">")
151
213
  # In Open API `items` value should be an object and not an array
152
- items = fast_deepcopy((schema or {}).get("items", {}))
214
+ items = deepclone((schema or {}).get("items", {}))
153
215
  child_options = items.get("xml", {})
154
216
  child_tag = child_options.get("name", tag)
155
217
  if not is_namespace_specified and "namespace" in options:
@@ -0,0 +1,165 @@
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.python import wsgi
13
+ from schemathesis.transport import BaseTransport, SerializationContext
14
+ from schemathesis.transport.prepare import normalize_base_url, prepare_body, prepare_headers, prepare_path
15
+ from schemathesis.transport.requests import REQUESTS_TRANSPORT
16
+ from schemathesis.transport.serialization import serialize_binary, serialize_json, serialize_xml, serialize_yaml
17
+
18
+ if TYPE_CHECKING:
19
+ import werkzeug
20
+
21
+
22
+ class WSGITransport(BaseTransport["Case", Response, "werkzeug.Client"]):
23
+ def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
24
+ headers = kwargs.get("headers")
25
+ params = kwargs.get("params")
26
+
27
+ final_headers = prepare_headers(case, headers)
28
+
29
+ media_type = case.media_type
30
+
31
+ # Set content type for payload
32
+ if media_type and not isinstance(case.body, NotSet):
33
+ final_headers["Content-Type"] = media_type
34
+
35
+ extra: dict[str, Any]
36
+ # Handle serialization
37
+ if not isinstance(case.body, NotSet) and media_type is not None:
38
+ serializer = self._get_serializer(media_type)
39
+ context = SerializationContext(case=case)
40
+ extra = serializer(context, prepare_body(case))
41
+ else:
42
+ extra = {}
43
+
44
+ data = {
45
+ "method": case.method,
46
+ "path": case.operation.schema.get_full_path(prepare_path(case.path, case.path_parameters)),
47
+ # Convert to regular dict for Werkzeug compatibility
48
+ "headers": dict(final_headers),
49
+ "query_string": case.query,
50
+ **extra,
51
+ }
52
+
53
+ if params is not None:
54
+ merge_at(data, "query_string", params)
55
+
56
+ return data
57
+
58
+ def send(
59
+ self,
60
+ case: Case,
61
+ *,
62
+ session: werkzeug.Client | None = None,
63
+ **kwargs: Any,
64
+ ) -> Response:
65
+ import requests
66
+
67
+ headers = kwargs.pop("headers", None)
68
+ params = kwargs.pop("params", None)
69
+ cookies = kwargs.pop("cookies", None)
70
+ application = kwargs.pop("app")
71
+
72
+ data = self.serialize_case(case, headers=headers, params=params)
73
+ data.update({key: value for key, value in kwargs.items() if key not in data})
74
+
75
+ client = session or wsgi.get_client(application)
76
+ cookies = {**(case.cookies or {}), **(cookies or {})}
77
+
78
+ with (
79
+ cookie_handler(client, cookies),
80
+ ratelimit(case.operation.schema.rate_limiter, case.operation.schema.base_url),
81
+ ):
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
+ )
104
+
105
+
106
+ @contextmanager
107
+ def cookie_handler(client: werkzeug.Client, cookies: dict[str, Any] | None) -> Generator[None, None, None]:
108
+ """Set cookies required for a call."""
109
+ if not cookies:
110
+ yield
111
+ else:
112
+ for key, value in cookies.items():
113
+ client.set_cookie(key=key, value=value, domain="localhost")
114
+ yield
115
+ for key in cookies:
116
+ client.delete_cookie(key=key, domain="localhost")
117
+
118
+
119
+ WSGI_TRANSPORT = WSGITransport()
120
+
121
+
122
+ @WSGI_TRANSPORT.serializer("application/json", "text/json")
123
+ def json_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
124
+ return serialize_json(value)
125
+
126
+
127
+ @WSGI_TRANSPORT.serializer(
128
+ "text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
129
+ )
130
+ def yaml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
131
+ return serialize_yaml(value)
132
+
133
+
134
+ @WSGI_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
135
+ def multipart_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
136
+ return {"data": value}
137
+
138
+
139
+ @WSGI_TRANSPORT.serializer("application/xml", "text/xml")
140
+ def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
141
+ media_type = ctx.case.media_type
142
+
143
+ assert media_type is not None
144
+
145
+ raw_schema = ctx.case.operation.get_raw_payload_schema(media_type)
146
+ resolved_schema = ctx.case.operation.get_resolved_payload_schema(media_type)
147
+
148
+ return serialize_xml(value, raw_schema, resolved_schema)
149
+
150
+
151
+ @WSGI_TRANSPORT.serializer("application/x-www-form-urlencoded")
152
+ def urlencoded_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
153
+ return {"data": value}
154
+
155
+
156
+ @WSGI_TRANSPORT.serializer("text/plain")
157
+ def text_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
158
+ if isinstance(value, bytes):
159
+ return {"data": value}
160
+ return {"data": str(value)}
161
+
162
+
163
+ @WSGI_TRANSPORT.serializer("application/octet-stream")
164
+ def binary_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
165
+ return {"data": serialize_binary(value)}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 3.39.7
3
+ Version: 4.0.0a2
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -20,7 +20,6 @@ Classifier: Intended Audience :: Developers
20
20
  Classifier: License :: OSI Approved :: MIT License
21
21
  Classifier: Operating System :: OS Independent
22
22
  Classifier: Programming Language :: Python :: 3 :: Only
23
- Classifier: Programming Language :: Python :: 3.8
24
23
  Classifier: Programming Language :: Python :: 3.9
25
24
  Classifier: Programming Language :: Python :: 3.10
26
25
  Classifier: Programming Language :: Python :: 3.11
@@ -28,29 +27,26 @@ Classifier: Programming Language :: Python :: 3.12
28
27
  Classifier: Programming Language :: Python :: 3.13
29
28
  Classifier: Programming Language :: Python :: Implementation :: CPython
30
29
  Classifier: Topic :: Software Development :: Testing
31
- Requires-Python: >=3.8
30
+ Requires-Python: >=3.9
32
31
  Requires-Dist: backoff<3.0,>=2.1.2
33
- Requires-Dist: click<9.0,>=7.0
32
+ Requires-Dist: click<9.0,>=8.0
34
33
  Requires-Dist: colorama<1.0,>=0.4
35
34
  Requires-Dist: harfile<1.0,>=0.3.0
36
35
  Requires-Dist: httpx<1.0,>=0.22.0
37
36
  Requires-Dist: hypothesis-graphql<1,>=0.11.1
38
37
  Requires-Dist: hypothesis-jsonschema<0.24,>=0.23.1
39
- Requires-Dist: hypothesis<7,>=6.103.4; python_version > '3.8'
40
- Requires-Dist: hypothesis[zoneinfo]<7,>=6.103.4; python_version == '3.8'
38
+ Requires-Dist: hypothesis<7,>=6.108.0
41
39
  Requires-Dist: jsonschema[format]<5.0,>=4.18.0
42
40
  Requires-Dist: junit-xml<2.0,>=1.9
43
- Requires-Dist: pyrate-limiter<4.0,>=2.10
44
- Requires-Dist: pytest-subtests<0.15.0,>=0.2.1
45
- Requires-Dist: pytest<9,>=4.6.4
41
+ Requires-Dist: pyrate-limiter<4.0,>=3.0
42
+ Requires-Dist: pytest-subtests<0.15.0,>=0.11
43
+ Requires-Dist: pytest<9,>=8
46
44
  Requires-Dist: pyyaml<7.0,>=5.1
47
45
  Requires-Dist: requests<3,>=2.22
46
+ Requires-Dist: rich>=13.9.4
48
47
  Requires-Dist: starlette-testclient<1,>=0.4.1
49
- Requires-Dist: starlette<1,>=0.13
50
- Requires-Dist: tomli-w<2.0,>=1.0.0
51
- Requires-Dist: tomli<3.0,>=2.0.1
48
+ Requires-Dist: typing-extensions>=4.12.2
52
49
  Requires-Dist: werkzeug<4,>=0.16.0
53
- Requires-Dist: yarl<2.0,>=1.5
54
50
  Provides-Extra: bench
55
51
  Requires-Dist: pytest-codspeed==2.2.1; extra == 'bench'
56
52
  Provides-Extra: cov
@@ -121,6 +117,14 @@ Description-Content-Type: text/markdown
121
117
 
122
118
  ## Schemathesis
123
119
 
120
+ > ⚠️ You are viewing the Schemathesis V4 README (Work in Progress) ⚠️
121
+
122
+ > This branch is under active development, with substantial changes expected before stabilization. While V4 is fully functional and passing tests, some features are missing, and the documentation may be outdated.
123
+
124
+ > For the stable release, see the [V3 branch](https://github.com/schemathesis/schemathesis/tree/v3).
125
+
126
+ > 💡 Have feedback? Share your thoughts in [this discussion](https://github.com/schemathesis/schemathesis/discussions/2677)!
127
+
124
128
  Schemathesis is an API testing tool that automatically finds crashes and validates spec compliance.
125
129
 
126
130
  <p align="center">
@@ -192,7 +196,7 @@ Or a Python library:
192
196
  ```python
193
197
  import schemathesis
194
198
 
195
- schema = schemathesis.from_uri("https://example.schemathesis.io/openapi.json")
199
+ schema = schemathesis.openapi.from_url("https://example.schemathesis.io/openapi.json")
196
200
 
197
201
 
198
202
  @schema.parametrize()