schemathesis 3.39.16__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 +233 -307
  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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.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 -717
  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.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,369 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import inspect
5
- import time
6
- from contextlib import contextmanager
7
- from dataclasses import dataclass
8
- from datetime import timedelta
9
- from inspect import iscoroutinefunction
10
- from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
11
- from urllib.parse import urlparse
12
-
13
- from .. import failures
14
- from .._dependency_versions import IS_WERKZEUG_ABOVE_3
15
- from ..constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET
16
- from ..exceptions import get_timeout_error
17
- from ..internal.copy import fast_deepcopy
18
- from ..serializers import SerializerContext
19
- from ..types import Cookies, NotSet, RequestCert
20
-
21
- if TYPE_CHECKING:
22
- import requests
23
- import werkzeug
24
- from _typeshed.wsgi import WSGIApplication
25
- from starlette_testclient._testclient import ASGI2App, ASGI3App
26
-
27
- from ..models import Case
28
- from .responses import WSGIResponse
29
-
30
-
31
- @dataclass
32
- class RequestConfig:
33
- timeout: int | None = None
34
- tls_verify: bool | str = True
35
- proxy: str | None = None
36
- cert: RequestCert | None = None
37
-
38
- def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
39
-
40
- @property
41
- def prepared_timeout(self) -> float | None:
42
- return prepare_timeout(self.timeout)
43
-
44
-
45
- def serialize_payload(payload: bytes) -> str:
46
- return base64.b64encode(payload).decode()
47
-
48
-
49
- def deserialize_payload(data: str | None) -> bytes | None:
50
- if data is None:
51
- return None
52
- return base64.b64decode(data)
53
-
54
-
55
- def get(app: Any) -> Transport:
56
- """Get transport to send the data to the application."""
57
- if app is None:
58
- return RequestsTransport()
59
- if iscoroutinefunction(app) or (
60
- hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
61
- ):
62
- return ASGITransport(app=app)
63
- return WSGITransport(app=app)
64
-
65
-
66
- S = TypeVar("S", contravariant=True)
67
- R = TypeVar("R", covariant=True)
68
-
69
-
70
- class Transport(Protocol[S, R]):
71
- def serialize_case(
72
- self,
73
- case: Case,
74
- *,
75
- base_url: str | None = None,
76
- headers: dict[str, Any] | None = None,
77
- params: dict[str, Any] | None = None,
78
- cookies: dict[str, Any] | None = None,
79
- ) -> dict[str, Any]:
80
- raise NotImplementedError
81
-
82
- def send(
83
- self,
84
- case: Case,
85
- *,
86
- session: S | None = None,
87
- base_url: str | None = None,
88
- headers: dict[str, Any] | None = None,
89
- params: dict[str, Any] | None = None,
90
- cookies: dict[str, Any] | None = None,
91
- **kwargs: Any,
92
- ) -> R:
93
- raise NotImplementedError
94
-
95
-
96
- class RequestsTransport:
97
- def serialize_case(
98
- self,
99
- case: Case,
100
- *,
101
- base_url: str | None = None,
102
- headers: dict[str, Any] | None = None,
103
- params: dict[str, Any] | None = None,
104
- cookies: dict[str, Any] | None = None,
105
- ) -> dict[str, Any]:
106
- final_headers = case._get_headers(headers)
107
- media_type: str | None
108
- if case.body is not NOT_SET and case.media_type is None:
109
- media_type = case.operation._get_default_media_type()
110
- else:
111
- media_type = case.media_type
112
- if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
113
- # `requests` will handle multipart form headers with the proper `boundary` value.
114
- if "content-type" not in final_headers:
115
- final_headers["Content-Type"] = media_type
116
- url = case._get_url(base_url)
117
- serializer = case._get_serializer(media_type)
118
- if serializer is not None and not isinstance(case.body, NotSet):
119
- context = SerializerContext(case=case)
120
- extra = serializer.as_requests(context, case._get_body())
121
- else:
122
- extra = {}
123
- if case._auth is not None:
124
- extra["auth"] = case._auth
125
- additional_headers = extra.pop("headers", None)
126
- if additional_headers:
127
- # Additional headers, needed for the serializer
128
- for key, value in additional_headers.items():
129
- final_headers.setdefault(key, value)
130
-
131
- p = case.query
132
-
133
- # Replace empty dictionaries with empty strings, so the parameters actually present in the query string
134
- if any(value == {} for value in (p or {}).values()):
135
- p = fast_deepcopy(p)
136
- for k, v in p.items():
137
- if v == {}:
138
- p[k] = ""
139
- data = {
140
- "method": case.method,
141
- "url": url,
142
- "cookies": case.cookies,
143
- "headers": final_headers,
144
- "params": p,
145
- **extra,
146
- }
147
- if params is not None:
148
- _merge_dict_to(data, "params", params)
149
- if cookies is not None:
150
- _merge_dict_to(data, "cookies", cookies)
151
- return data
152
-
153
- def send(
154
- self,
155
- case: Case,
156
- *,
157
- session: requests.Session | None = None,
158
- base_url: str | None = None,
159
- headers: dict[str, Any] | None = None,
160
- params: dict[str, Any] | None = None,
161
- cookies: dict[str, Any] | None = None,
162
- **kwargs: Any,
163
- ) -> requests.Response:
164
- import requests
165
- from urllib3.exceptions import ReadTimeoutError
166
-
167
- data = self.serialize_case(case, base_url=base_url, headers=headers, params=params, cookies=cookies)
168
- data.update(kwargs)
169
- data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
170
- if session is None:
171
- validate_vanilla_requests_kwargs(data)
172
- session = requests.Session()
173
- close_session = True
174
- else:
175
- close_session = False
176
- verify = data.get("verify", True)
177
- try:
178
- with case.operation.schema.ratelimit():
179
- response = session.request(**data) # type: ignore
180
- except (requests.Timeout, requests.ConnectionError) as exc:
181
- if isinstance(exc, requests.ConnectionError):
182
- if not isinstance(exc.args[0], ReadTimeoutError):
183
- raise
184
- req = requests.Request(
185
- method=data["method"].upper(),
186
- url=data["url"],
187
- headers=data["headers"],
188
- files=data.get("files"),
189
- data=data.get("data") or {},
190
- json=data.get("json"),
191
- params=data.get("params") or {},
192
- auth=data.get("auth"),
193
- cookies=data["cookies"],
194
- hooks=data.get("hooks"),
195
- )
196
- request = session.prepare_request(req)
197
- else:
198
- request = cast(requests.PreparedRequest, exc.request)
199
- timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
200
- code_message = case._get_code_message(case.operation.schema.code_sample_style, request, verify=verify)
201
- message = f"The server failed to respond within the specified limit of {timeout:.2f}ms"
202
- raise get_timeout_error(case.operation.verbose_name, timeout)(
203
- f"\n\n1. {failures.RequestTimeout.title}\n\n{message}\n\n{code_message}",
204
- context=failures.RequestTimeout(message=message, timeout=timeout),
205
- ) from None
206
- response.verify = verify # type: ignore[attr-defined]
207
- response._session = session # type: ignore[attr-defined]
208
- if close_session:
209
- session.close()
210
- return response
211
-
212
-
213
- def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
214
- original = data[data_key] or {}
215
- for key, value in new.items():
216
- original[key] = value
217
- data[data_key] = original
218
-
219
-
220
- def prepare_timeout(timeout: int | None) -> float | None:
221
- """Request timeout is in milliseconds, but `requests` uses seconds."""
222
- output: int | float | None = timeout
223
- if timeout is not None:
224
- output = timeout / 1000
225
- return output
226
-
227
-
228
- def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
229
- """Check arguments for `requests.Session.request`.
230
-
231
- Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
232
- `requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
233
- """
234
- url = data["url"]
235
- if not urlparse(url).netloc:
236
- stack = inspect.stack()
237
- method_name = "call"
238
- for frame in stack[1:]:
239
- if frame.function == "call_and_validate":
240
- method_name = "call_and_validate"
241
- break
242
- raise RuntimeError(
243
- "The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
244
- f"Pass `base_url` either to the `schemathesis.from_*` loader or to the `Case.{method_name}`.\n"
245
- f"If you use the ASGI integration, please supply your test client "
246
- f"as the `session` argument to `call`.\nURL: {url}"
247
- )
248
-
249
-
250
- @dataclass
251
- class ASGITransport(RequestsTransport):
252
- app: ASGI2App | ASGI3App
253
-
254
- def send(
255
- self,
256
- case: Case,
257
- *,
258
- session: requests.Session | None = None,
259
- base_url: str | None = None,
260
- headers: dict[str, Any] | None = None,
261
- params: dict[str, Any] | None = None,
262
- cookies: dict[str, Any] | None = None,
263
- **kwargs: Any,
264
- ) -> requests.Response:
265
- from starlette_testclient import TestClient as ASGIClient
266
-
267
- if base_url is None:
268
- base_url = case.get_full_base_url()
269
- with ASGIClient(self.app) as client:
270
- return super().send(
271
- case, session=client, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
272
- )
273
-
274
-
275
- @dataclass
276
- class WSGITransport:
277
- app: WSGIApplication
278
-
279
- def serialize_case(
280
- self,
281
- case: Case,
282
- *,
283
- base_url: str | None = None,
284
- headers: dict[str, Any] | None = None,
285
- params: dict[str, Any] | None = None,
286
- cookies: dict[str, Any] | None = None,
287
- ) -> dict[str, Any]:
288
- final_headers = case._get_headers(headers)
289
- media_type: str | None
290
- if case.body is not NOT_SET and case.media_type is None:
291
- media_type = case.operation._get_default_media_type()
292
- else:
293
- media_type = case.media_type
294
- if media_type and not isinstance(case.body, NotSet):
295
- # If we need to send a payload, then the Content-Type header should be set
296
- final_headers["Content-Type"] = media_type
297
- extra: dict[str, Any]
298
- serializer = case._get_serializer(media_type)
299
- if serializer is not None and not isinstance(case.body, NotSet):
300
- context = SerializerContext(case=case)
301
- extra = serializer.as_werkzeug(context, case._get_body())
302
- else:
303
- extra = {}
304
- data = {
305
- "method": case.method,
306
- "path": case.operation.schema.get_full_path(case.formatted_path),
307
- # Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
308
- "headers": dict(final_headers),
309
- "query_string": case.query,
310
- **extra,
311
- }
312
- if params is not None:
313
- _merge_dict_to(data, "query_string", params)
314
- return data
315
-
316
- def send(
317
- self,
318
- case: Case,
319
- *,
320
- session: Any = None,
321
- base_url: str | None = None,
322
- headers: dict[str, Any] | None = None,
323
- params: dict[str, Any] | None = None,
324
- cookies: dict[str, Any] | None = None,
325
- **kwargs: Any,
326
- ) -> WSGIResponse:
327
- import requests
328
- import werkzeug
329
-
330
- from .responses import WSGIResponse
331
-
332
- application = kwargs.pop("app", self.app) or self.app
333
- data = self.serialize_case(case, headers=headers, params=params)
334
- data.update(kwargs)
335
- client = werkzeug.Client(application, WSGIResponse)
336
- cookies = {**(case.cookies or {}), **(cookies or {})}
337
- with cookie_handler(client, cookies), case.operation.schema.ratelimit():
338
- start = time.monotonic()
339
- response = client.open(**data)
340
- elapsed = time.monotonic() - start
341
- requests_kwargs = RequestsTransport().serialize_case(
342
- case,
343
- base_url=case.get_full_base_url(),
344
- headers=headers,
345
- params=params,
346
- cookies=cookies,
347
- )
348
- response.request = requests.Request(**requests_kwargs).prepare()
349
- response.elapsed = timedelta(seconds=elapsed)
350
- return response
351
-
352
-
353
- @contextmanager
354
- def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
355
- """Set cookies required for a call."""
356
- if not cookies:
357
- yield
358
- else:
359
- for key, value in cookies.items():
360
- if IS_WERKZEUG_ABOVE_3:
361
- client.set_cookie(key=key, value=value, domain="localhost")
362
- else:
363
- client.set_cookie("localhost", key=key, value=value)
364
- yield
365
- for key in cookies:
366
- if IS_WERKZEUG_ABOVE_3:
367
- client.delete_cookie(key=key, domain="localhost")
368
- else:
369
- client.delete_cookie("localhost", key=key)
@@ -1,7 +0,0 @@
1
- from inspect import iscoroutinefunction
2
-
3
-
4
- def is_asgi_app(app: object) -> bool:
5
- return iscoroutinefunction(app) or (
6
- hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
7
- )
@@ -1,38 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any
4
-
5
- from ..constants import USER_AGENT
6
-
7
- if TYPE_CHECKING:
8
- from requests.auth import HTTPDigestAuth
9
-
10
- from ..types import RawAuth
11
-
12
-
13
- def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
14
- from requests.auth import HTTPDigestAuth
15
-
16
- if auth and auth_type == "digest":
17
- return HTTPDigestAuth(*auth)
18
- return auth
19
-
20
-
21
- def prepare_wsgi_headers(headers: dict[str, Any] | None, auth: RawAuth | None, auth_type: str | None) -> dict[str, Any]:
22
- headers = headers or {}
23
- if "user-agent" not in {header.lower() for header in headers}:
24
- headers["User-Agent"] = USER_AGENT
25
- wsgi_auth = get_wsgi_auth(auth, auth_type)
26
- if wsgi_auth:
27
- headers["Authorization"] = wsgi_auth
28
- return headers
29
-
30
-
31
- def get_wsgi_auth(auth: RawAuth | None, auth_type: str | None) -> str | None:
32
- from requests.auth import _basic_auth_str
33
-
34
- if auth:
35
- if auth_type == "digest":
36
- raise ValueError("Digest auth is not supported for WSGI apps")
37
- return _basic_auth_str(*auth)
38
- return None
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import Any
5
-
6
- from ..constants import USER_AGENT
7
-
8
-
9
- def setup_default_headers(kwargs: dict[str, Any]) -> None:
10
- headers = kwargs.setdefault("headers", {})
11
- if "user-agent" not in {header.lower() for header in headers}:
12
- kwargs["headers"]["User-Agent"] = USER_AGENT
13
-
14
-
15
- def is_latin_1_encodable(value: str) -> bool:
16
- """Header values are encoded to latin-1 before sending."""
17
- try:
18
- value.encode("latin-1")
19
- return True
20
- except UnicodeEncodeError:
21
- return False
22
-
23
-
24
- # Adapted from http.client._is_illegal_header_value
25
- INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
26
-
27
-
28
- def has_invalid_characters(name: str, value: str) -> bool:
29
- from requests.exceptions import InvalidHeader
30
- from requests.utils import check_header_validity
31
-
32
- try:
33
- check_header_validity((name, value))
34
- return bool(INVALID_HEADER_RE.search(value))
35
- except InvalidHeader:
36
- return True
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import sys
5
- from typing import TYPE_CHECKING, Any, NoReturn, Union
6
-
7
- from werkzeug.wrappers import Response as BaseResponse
8
-
9
- from .._compat import JSONMixin
10
-
11
- if TYPE_CHECKING:
12
- from datetime import timedelta
13
-
14
- from httpx import Response as httpxResponse
15
- from requests import PreparedRequest
16
- from requests import Response as requestsResponse
17
-
18
-
19
- class WSGIResponse(BaseResponse, JSONMixin):
20
- # We store "requests" request to build a reproduction code
21
- request: PreparedRequest
22
- elapsed: timedelta
23
-
24
- def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
25
- # We don't need a werkzeug-specific exception when JSON parsing error happens
26
- raise e
27
-
28
-
29
- def get_payload(response: GenericResponse) -> str:
30
- from httpx import Response as httpxResponse
31
- from requests import Response as requestsResponse
32
-
33
- if isinstance(response, (httpxResponse, requestsResponse)):
34
- return response.text
35
- return response.get_data(as_text=True)
36
-
37
-
38
- def get_json(response: GenericResponse) -> Any:
39
- from httpx import Response as httpxResponse
40
- from requests import Response as requestsResponse
41
-
42
- if isinstance(response, (httpxResponse, requestsResponse)):
43
- return json.loads(response.text)
44
- return response.json
45
-
46
-
47
- def get_reason(status_code: int) -> str:
48
- if sys.version_info < (3, 9) and status_code == 418:
49
- # Python 3.8 does not have 418 status in the `HTTPStatus` enum
50
- return "I'm a Teapot"
51
-
52
- import http.client
53
-
54
- return http.client.responses.get(status_code, "Unknown")
55
-
56
-
57
- GenericResponse = Union["httpxResponse", "requestsResponse", WSGIResponse]
schemathesis/types.py DELETED
@@ -1,44 +0,0 @@
1
- import enum
2
- from pathlib import Path
3
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
4
-
5
- if TYPE_CHECKING:
6
- from hypothesis.strategies import SearchStrategy
7
-
8
- from .hooks import HookContext
9
-
10
- PathLike = Union[Path, str]
11
-
12
- Query = Dict[str, Any]
13
- # Body can be of any Python type that corresponds to JSON Schema types + `bytes`
14
- Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
15
- PathParameters = Dict[str, Any]
16
- Headers = Dict[str, Any]
17
- Cookies = Dict[str, Any]
18
- FormData = Dict[str, Any]
19
-
20
-
21
- class NotSet:
22
- pass
23
-
24
-
25
- RequestCert = Union[str, Tuple[str, str]]
26
-
27
-
28
- # A filter for path / method
29
- Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
30
-
31
- Hook = Union[
32
- Callable[["SearchStrategy"], "SearchStrategy"], Callable[["SearchStrategy", "HookContext"], "SearchStrategy"]
33
- ]
34
-
35
- RawAuth = Tuple[str, str]
36
- # Generic test with any arguments and no return
37
- GenericTest = Callable[..., None]
38
-
39
-
40
- class Specification(str, enum.Enum):
41
- """Specification of the given schema."""
42
-
43
- OPENAPI = "openapi"
44
- GRAPHQL = "graphql"