schemathesis 3.15.4__py3-none-any.whl → 4.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from typing import TYPE_CHECKING, Any, Mapping
6
+
7
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
8
+
9
+ if TYPE_CHECKING:
10
+ import httpx
11
+ import requests
12
+ from werkzeug.test import TestResponse
13
+
14
+ from schemathesis.generation.overrides import Override
15
+
16
+ USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
17
+ DEFAULT_RESPONSE_TIMEOUT = 10
18
+
19
+
20
+ def prepare_urlencoded(data: Any) -> Any:
21
+ if isinstance(data, list):
22
+ output = []
23
+ for item in data:
24
+ if isinstance(item, dict):
25
+ for key, value in item.items():
26
+ output.append((key, value))
27
+ else:
28
+ output.append((item, "arbitrary-value"))
29
+ return output
30
+ return data
31
+
32
+
33
+ class Response:
34
+ """HTTP response wrapper that normalizes different transport implementations.
35
+
36
+ Provides a consistent interface for accessing response data whether the request
37
+ was made via HTTP, ASGI, or WSGI transports.
38
+ """
39
+
40
+ status_code: int
41
+ """HTTP status code (e.g., 200, 404, 500)."""
42
+ headers: dict[str, list[str]]
43
+ """Response headers with lowercase keys and list values."""
44
+ content: bytes
45
+ """Raw response body as bytes."""
46
+ request: requests.PreparedRequest
47
+ """The request that generated this response."""
48
+ elapsed: float
49
+ """Response time in seconds."""
50
+ verify: bool
51
+ """Whether TLS verification was enabled for the request."""
52
+ message: str
53
+ """HTTP status message (e.g., "OK", "Not Found")."""
54
+ http_version: str
55
+ """HTTP protocol version ("1.0" or "1.1")."""
56
+ encoding: str | None
57
+ """Character encoding for text content, if detected."""
58
+ _override: Override | None
59
+
60
+ __slots__ = (
61
+ "status_code",
62
+ "headers",
63
+ "content",
64
+ "request",
65
+ "elapsed",
66
+ "verify",
67
+ "_json",
68
+ "message",
69
+ "http_version",
70
+ "encoding",
71
+ "_encoded_body",
72
+ "_override",
73
+ )
74
+
75
+ def __init__(
76
+ self,
77
+ status_code: int,
78
+ headers: Mapping[str, list[str]],
79
+ content: bytes,
80
+ request: requests.PreparedRequest,
81
+ elapsed: float,
82
+ verify: bool,
83
+ message: str = "",
84
+ http_version: str = "1.1",
85
+ encoding: str | None = None,
86
+ _override: Override | None = None,
87
+ ):
88
+ self.status_code = status_code
89
+ self.headers = {key.lower(): value for key, value in headers.items()}
90
+ assert all(isinstance(v, list) for v in headers.values())
91
+ self.content = content
92
+ self.request = request
93
+ self.elapsed = elapsed
94
+ self.verify = verify
95
+ self._json = None
96
+ self._encoded_body: str | None = None
97
+ self.message = message
98
+ self.http_version = http_version
99
+ self.encoding = encoding
100
+ self._override = _override
101
+
102
+ @classmethod
103
+ def from_any(cls, response: Response | httpx.Response | requests.Response | TestResponse) -> Response:
104
+ import httpx
105
+ import requests
106
+ from werkzeug.test import TestResponse
107
+
108
+ if isinstance(response, requests.Response):
109
+ return Response.from_requests(response, verify=True)
110
+ elif isinstance(response, httpx.Response):
111
+ return Response.from_httpx(response, verify=True)
112
+ elif isinstance(response, TestResponse):
113
+ return Response.from_wsgi(response)
114
+ return response
115
+
116
+ @classmethod
117
+ def from_requests(cls, response: requests.Response, verify: bool, _override: Override | None = None) -> Response:
118
+ raw = response.raw
119
+ raw_headers = raw.headers if raw is not None else {}
120
+ headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
121
+ # Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
122
+ version = raw.version if raw is not None else 10
123
+ http_version = "1.0" if version == 10 else "1.1"
124
+ return Response(
125
+ status_code=response.status_code,
126
+ headers=headers,
127
+ content=response.content,
128
+ request=response.request,
129
+ elapsed=response.elapsed.total_seconds(),
130
+ message=response.reason,
131
+ encoding=response.encoding,
132
+ http_version=http_version,
133
+ verify=verify,
134
+ _override=_override,
135
+ )
136
+
137
+ @classmethod
138
+ def from_httpx(cls, response: httpx.Response, verify: bool) -> Response:
139
+ import requests
140
+
141
+ request = requests.Request(
142
+ method=response.request.method,
143
+ url=str(response.request.url),
144
+ headers=dict(response.request.headers),
145
+ params=dict(response.request.url.params),
146
+ data=response.request.content,
147
+ ).prepare()
148
+ return Response(
149
+ status_code=response.status_code,
150
+ headers={key: [value] for key, value in response.headers.items()},
151
+ content=response.content,
152
+ request=request,
153
+ elapsed=response.elapsed.total_seconds(),
154
+ message=response.reason_phrase,
155
+ encoding=response.encoding,
156
+ http_version=response.http_version,
157
+ verify=verify,
158
+ )
159
+
160
+ @classmethod
161
+ def from_wsgi(cls, response: TestResponse) -> Response:
162
+ import http.client
163
+
164
+ import requests
165
+
166
+ reason = http.client.responses.get(response.status_code, "Unknown")
167
+ data = response.get_data()
168
+ if response.response == []:
169
+ # Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
170
+ encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
171
+ else:
172
+ encoding = None
173
+ request = requests.Request(
174
+ method=response.request.method,
175
+ url=str(response.request.url),
176
+ headers=dict(response.request.headers),
177
+ params=dict(response.request.args),
178
+ # Request body is not available
179
+ data=b"",
180
+ ).prepare()
181
+ return Response(
182
+ status_code=response.status_code,
183
+ headers={name: response.headers.getlist(name) for name in response.headers.keys()},
184
+ content=data,
185
+ request=request,
186
+ # Elapsed time is not available
187
+ elapsed=0.0,
188
+ message=reason,
189
+ encoding=encoding,
190
+ http_version="1.1",
191
+ verify=False,
192
+ )
193
+
194
+ @property
195
+ def text(self) -> str:
196
+ """Decode response content as text using the detected or default encoding."""
197
+ return self.content.decode(self.encoding if self.encoding else "utf-8")
198
+
199
+ def json(self) -> Any:
200
+ """Parse response content as JSON.
201
+
202
+ Returns:
203
+ Parsed JSON data (dict, list, or primitive types)
204
+
205
+ Raises:
206
+ json.JSONDecodeError: If content is not valid JSON
207
+
208
+ """
209
+ if self._json is None:
210
+ self._json = json.loads(self.text)
211
+ return self._json
212
+
213
+ @property
214
+ def body_size(self) -> int | None:
215
+ """Size of response body in bytes, or None if no content."""
216
+ return len(self.content) if self.content else None
217
+
218
+ @property
219
+ def encoded_body(self) -> str | None:
220
+ """Base64-encoded response body for binary-safe serialization."""
221
+ if self._encoded_body is None and self.content:
222
+ self._encoded_body = base64.b64encode(self.content).decode()
223
+ return self._encoded_body
@@ -0,0 +1,73 @@
1
+ import re
2
+ from urllib.parse import urlparse
3
+
4
+ from schemathesis.core.errors import InvalidSchema
5
+
6
+ # Adapted from http.client._is_illegal_header_value
7
+ INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
8
+
9
+
10
+ def has_invalid_characters(name: str, value: object) -> bool:
11
+ from requests.exceptions import InvalidHeader
12
+ from requests.utils import check_header_validity
13
+
14
+ if not isinstance(value, str):
15
+ return False
16
+ try:
17
+ check_header_validity((name, value))
18
+ return bool(INVALID_HEADER_RE.search(value))
19
+ except InvalidHeader:
20
+ return True
21
+
22
+
23
+ def is_latin_1_encodable(value: object) -> bool:
24
+ """Check if a value is a Latin-1 encodable string."""
25
+ if not isinstance(value, str):
26
+ return False
27
+ try:
28
+ value.encode("latin-1")
29
+ return True
30
+ except UnicodeEncodeError:
31
+ return False
32
+
33
+
34
+ def check_header_name(name: str) -> None:
35
+ from requests.exceptions import InvalidHeader
36
+ from requests.utils import check_header_validity
37
+
38
+ if not name:
39
+ raise InvalidSchema("Header name should not be empty")
40
+ if not name.isascii():
41
+ # `urllib3` encodes header names to ASCII
42
+ raise InvalidSchema(f"Header name should be ASCII: {name}")
43
+ try:
44
+ check_header_validity((name, ""))
45
+ except InvalidHeader as exc:
46
+ raise InvalidSchema(str(exc)) from None
47
+ if bool(INVALID_HEADER_RE.search(name)):
48
+ raise InvalidSchema(f"Invalid header name: {name}")
49
+
50
+
51
+ SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
52
+ _contains_surrogate_pair = SURROGATE_PAIR_RE.search
53
+
54
+
55
+ def contains_unicode_surrogate_pair(item: object) -> bool:
56
+ if isinstance(item, list):
57
+ return any(isinstance(item_, str) and bool(_contains_surrogate_pair(item_)) for item_ in item)
58
+ return isinstance(item, str) and bool(_contains_surrogate_pair(item))
59
+
60
+
61
+ INVALID_BASE_URL_MESSAGE = (
62
+ "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
63
+ "Make sure it is a properly formatted URL."
64
+ )
65
+
66
+
67
+ def validate_base_url(value: str) -> None:
68
+ try:
69
+ netloc = urlparse(value).netloc
70
+ except ValueError as exc:
71
+ raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
72
+ if value and not netloc:
73
+ raise ValueError(INVALID_BASE_URL_MESSAGE)
@@ -0,0 +1,7 @@
1
+ from importlib import metadata
2
+
3
+ try:
4
+ SCHEMATHESIS_VERSION = metadata.version("schemathesis")
5
+ except metadata.PackageNotFoundError:
6
+ # Local run without installation
7
+ SCHEMATHESIS_VERSION = "dev"
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from schemathesis.engine.core import Engine
8
+ from schemathesis.schemas import BaseSchema
9
+
10
+
11
+ class Status(str, Enum):
12
+ SUCCESS = "success"
13
+ FAILURE = "failure"
14
+ ERROR = "error"
15
+ INTERRUPTED = "interrupted"
16
+ SKIP = "skip"
17
+
18
+ def __lt__(self, other: Status) -> bool: # type: ignore[override]
19
+ return _STATUS_ORDER[self] < _STATUS_ORDER[other]
20
+
21
+
22
+ _STATUS_ORDER = {Status.SUCCESS: 0, Status.FAILURE: 1, Status.ERROR: 2, Status.INTERRUPTED: 3, Status.SKIP: 4}
23
+
24
+
25
+ def from_schema(schema: BaseSchema) -> Engine:
26
+ from .core import Engine
27
+
28
+ return Engine(schema=schema)
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schemathesis.config import ProjectConfig
8
+ from schemathesis.core import NOT_SET, NotSet
9
+ from schemathesis.engine.control import ExecutionControl
10
+ from schemathesis.engine.observations import Observations
11
+ from schemathesis.generation.case import Case
12
+ from schemathesis.schemas import APIOperation, BaseSchema
13
+
14
+ if TYPE_CHECKING:
15
+ import threading
16
+
17
+ import requests
18
+
19
+ from schemathesis.engine.recorder import ScenarioRecorder
20
+
21
+
22
+ @dataclass
23
+ class EngineContext:
24
+ """Holds context shared for a test run."""
25
+
26
+ schema: BaseSchema
27
+ control: ExecutionControl
28
+ outcome_cache: dict[int, BaseException | None]
29
+ start_time: float
30
+ observations: Observations | None
31
+
32
+ __slots__ = (
33
+ "schema",
34
+ "control",
35
+ "outcome_cache",
36
+ "start_time",
37
+ "observations",
38
+ "_session",
39
+ "_transport_kwargs_cache",
40
+ )
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ schema: BaseSchema,
46
+ stop_event: threading.Event,
47
+ observations: Observations | None = None,
48
+ session: requests.Session | None = None,
49
+ ) -> None:
50
+ self.schema = schema
51
+ self.control = ExecutionControl(stop_event=stop_event, max_failures=schema.config.max_failures)
52
+ self.outcome_cache = {}
53
+ self.start_time = time.monotonic()
54
+ self.observations = observations
55
+ self._session = session
56
+ self._transport_kwargs_cache: dict[str | None, dict[str, Any]] = {}
57
+
58
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
59
+
60
+ @property
61
+ def config(self) -> ProjectConfig:
62
+ return self.schema.config
63
+
64
+ @property
65
+ def running_time(self) -> float:
66
+ return time.monotonic() - self.start_time
67
+
68
+ @property
69
+ def has_to_stop(self) -> bool:
70
+ """Check if execution should stop."""
71
+ return self.control.is_stopped
72
+
73
+ @property
74
+ def is_interrupted(self) -> bool:
75
+ return self.control.is_interrupted
76
+
77
+ @property
78
+ def has_reached_the_failure_limit(self) -> bool:
79
+ return self.control.has_reached_the_failure_limit
80
+
81
+ def record_observations(self, recorder: ScenarioRecorder) -> None:
82
+ """Add new observations from a scenario."""
83
+ if self.observations is not None:
84
+ self.observations.extract_observations_from(recorder)
85
+
86
+ def inject_links(self) -> int:
87
+ """Inject inferred OpenAPI links into API operations based on collected observations."""
88
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
89
+
90
+ injected = 0
91
+ if self.observations is not None and self.observations.location_headers:
92
+ assert isinstance(self.schema, BaseOpenAPISchema)
93
+
94
+ # Generate links from collected Location headers
95
+ for operation, entries in self.observations.location_headers.items():
96
+ injected += self.schema.analysis.inferencer.inject_links(operation.responses, entries)
97
+ if isinstance(self.schema, BaseOpenAPISchema) and self.schema.analysis.should_inject_links():
98
+ injected += self.schema.analysis.inject_links()
99
+ return injected
100
+
101
+ def stop(self) -> None:
102
+ self.control.stop()
103
+
104
+ def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
105
+ self.outcome_cache[hash(case)] = outcome
106
+
107
+ def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
108
+ return self.outcome_cache.get(hash(case), NOT_SET)
109
+
110
+ def get_session(self, *, operation: APIOperation | None = None) -> requests.Session:
111
+ if self._session is not None:
112
+ return self._session
113
+ import requests
114
+
115
+ session = requests.Session()
116
+ session.headers = {}
117
+ config = self.config
118
+
119
+ session.verify = config.tls_verify_for(operation=operation)
120
+ auth = config.auth_for(operation=operation)
121
+ if auth is not None:
122
+ session.auth = auth
123
+ headers = config.headers_for(operation=operation)
124
+ if headers:
125
+ session.headers.update(headers)
126
+ request_cert = config.request_cert_for(operation=operation)
127
+ if request_cert is not None:
128
+ session.cert = request_cert
129
+ proxy = config.proxy_for(operation=operation)
130
+ if proxy is not None:
131
+ session.proxies["all"] = proxy
132
+ return session
133
+
134
+ def get_transport_kwargs(self, operation: APIOperation | None = None) -> dict[str, Any]:
135
+ key = operation.label if operation is not None else None
136
+ cached = self._transport_kwargs_cache.get(key)
137
+ if cached is not None:
138
+ return cached.copy()
139
+ config = self.config
140
+ kwargs: dict[str, Any] = {
141
+ "session": self.get_session(operation=operation),
142
+ "headers": config.headers_for(operation=operation),
143
+ "max_redirects": config.max_redirects_for(operation=operation),
144
+ "timeout": config.request_timeout_for(operation=operation),
145
+ "verify": config.tls_verify_for(operation=operation),
146
+ "cert": config.request_cert_for(operation=operation),
147
+ }
148
+ proxy = config.proxy_for(operation=operation)
149
+ if proxy is not None:
150
+ kwargs["proxies"] = {"all": proxy}
151
+ self._transport_kwargs_cache[key] = kwargs
152
+ return kwargs
@@ -0,0 +1,44 @@
1
+ """Control for the Schemathesis Engine execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class ExecutionControl:
11
+ """Controls engine execution flow and tracks failures."""
12
+
13
+ stop_event: threading.Event
14
+ max_failures: int | None
15
+ _failures_counter: int
16
+ has_reached_the_failure_limit: bool
17
+
18
+ __slots__ = ("stop_event", "max_failures", "_failures_counter", "has_reached_the_failure_limit")
19
+
20
+ def __init__(self, stop_event: threading.Event, max_failures: int | None) -> None:
21
+ self.stop_event = stop_event
22
+ self.max_failures = max_failures
23
+ self._failures_counter = 0
24
+ self.has_reached_the_failure_limit = False
25
+
26
+ @property
27
+ def is_stopped(self) -> bool:
28
+ """Check if execution should stop."""
29
+ return self.is_interrupted or self.has_reached_the_failure_limit
30
+
31
+ @property
32
+ def is_interrupted(self) -> bool:
33
+ return self.stop_event.is_set()
34
+
35
+ def stop(self) -> None:
36
+ """Signal to stop execution."""
37
+ self.stop_event.set()
38
+
39
+ def count_failure(self) -> None:
40
+ # N failures limit
41
+ if self.max_failures is not None:
42
+ self._failures_counter += 1
43
+ if self._failures_counter >= self.max_failures:
44
+ self.has_reached_the_failure_limit = True