schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,411 +0,0 @@
1
- """Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
2
-
3
- They all consist of primitive types and don't have references to schemas, app, etc.
4
- """
5
- from __future__ import annotations
6
- import logging
7
- import re
8
- from dataclasses import dataclass, field
9
- from typing import Any, TYPE_CHECKING, cast
10
-
11
- from ..transports import serialize_payload
12
- from ..code_samples import get_excluded_headers
13
- from ..exceptions import (
14
- FailureContext,
15
- InternalError,
16
- make_unique_by_key,
17
- format_exception,
18
- extract_requests_exception_details,
19
- RuntimeErrorType,
20
- DeadlineExceeded,
21
- OperationSchemaError,
22
- BodyInGetRequestError,
23
- InvalidRegularExpression,
24
- SerializationError,
25
- UnboundPrefixError,
26
- )
27
- from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
28
-
29
- if TYPE_CHECKING:
30
- import hypothesis.errors
31
- from requests.structures import CaseInsensitiveDict
32
-
33
-
34
- @dataclass
35
- class SerializedCase:
36
- # Case data
37
- id: str
38
- path_parameters: dict[str, Any] | None
39
- headers: dict[str, Any] | None
40
- cookies: dict[str, Any] | None
41
- query: dict[str, Any] | None
42
- body: str | None
43
- media_type: str | None
44
- data_generation_method: str | None
45
- # Operation data
46
- method: str
47
- url: str
48
- path_template: str
49
- verbose_name: str
50
- # Transport info
51
- verify: bool
52
- # Headers coming from sources outside data generation
53
- extra_headers: dict[str, Any]
54
-
55
- @classmethod
56
- def from_case(cls, case: Case, headers: dict[str, Any] | None, verify: bool) -> SerializedCase:
57
- # `headers` include not only explicitly provided headers but also ones added by hooks, custom auth, etc.
58
- request_data = case.prepare_code_sample_data(headers)
59
- serialized_body = _serialize_body(request_data.body)
60
- return cls(
61
- id=case.id,
62
- path_parameters=case.path_parameters,
63
- headers=dict(case.headers) if case.headers is not None else None,
64
- cookies=case.cookies,
65
- query=case.query,
66
- body=serialized_body,
67
- media_type=case.media_type,
68
- data_generation_method=case.data_generation_method.as_short_name()
69
- if case.data_generation_method is not None
70
- else None,
71
- method=case.method,
72
- url=request_data.url,
73
- path_template=case.path,
74
- verbose_name=case.operation.verbose_name,
75
- verify=verify,
76
- extra_headers=request_data.headers,
77
- )
78
-
79
-
80
- def _serialize_body(body: str | bytes | None) -> str | None:
81
- if body is None:
82
- return None
83
- if isinstance(body, str):
84
- body = body.encode("utf-8")
85
- return serialize_payload(body)
86
-
87
-
88
- @dataclass
89
- class SerializedCheck:
90
- # Check name
91
- name: str
92
- # Check result
93
- value: Status
94
- request: Request
95
- response: Response | None
96
- # Generated example
97
- example: SerializedCase
98
- # Message could be absent for plain `assert` statements
99
- message: str | None = None
100
- # Failure-specific context
101
- context: FailureContext | None = None
102
- # Cases & responses that were made before this one
103
- history: list[SerializedHistoryEntry] = field(default_factory=list)
104
-
105
- @classmethod
106
- def from_check(cls, check: Check) -> SerializedCheck:
107
- import requests
108
- from ..transports.responses import WSGIResponse
109
-
110
- if check.response is not None:
111
- request = Request.from_prepared_request(check.response.request)
112
- elif check.request is not None:
113
- # Response is not available, but it is not an error (only time-out behaves this way at the moment)
114
- request = Request.from_prepared_request(check.request)
115
- else:
116
- raise InternalError("Can not find request data")
117
-
118
- response: Response | None
119
- if isinstance(check.response, requests.Response):
120
- response = Response.from_requests(check.response)
121
- elif isinstance(check.response, WSGIResponse):
122
- response = Response.from_wsgi(check.response, check.elapsed)
123
- else:
124
- response = None
125
- headers = _get_headers(request.headers)
126
- history = get_serialized_history(check.example)
127
- return cls(
128
- name=check.name,
129
- value=check.value,
130
- example=SerializedCase.from_case(
131
- check.example, headers, verify=response.verify if response is not None else True
132
- ),
133
- message=check.message,
134
- request=request,
135
- response=response,
136
- context=check.context,
137
- history=history,
138
- )
139
-
140
-
141
- def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
142
- return {key: value[0] for key, value in headers.items() if key not in get_excluded_headers()}
143
-
144
-
145
- @dataclass
146
- class SerializedHistoryEntry:
147
- case: SerializedCase
148
- response: Response
149
-
150
-
151
- def get_serialized_history(case: Case) -> list[SerializedHistoryEntry]:
152
- import requests
153
-
154
- history = []
155
- while case.source is not None:
156
- history_request = case.source.response.request
157
- headers = _get_headers(history_request.headers)
158
- if isinstance(case.source.response, requests.Response):
159
- history_response = Response.from_requests(case.source.response)
160
- verify = history_response.verify
161
- else:
162
- history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
163
- verify = True
164
- entry = SerializedHistoryEntry(
165
- case=SerializedCase.from_case(case.source.case, headers, verify=verify), response=history_response
166
- )
167
- history.append(entry)
168
- case = case.source.case
169
- return history
170
-
171
-
172
- @dataclass
173
- class SerializedError:
174
- type: RuntimeErrorType
175
- title: str | None
176
- message: str | None
177
- extras: list[str]
178
-
179
- # Exception info
180
- exception: str
181
- exception_with_traceback: str
182
-
183
- @classmethod
184
- def with_exception(
185
- cls,
186
- type_: RuntimeErrorType,
187
- title: str | None,
188
- message: str | None,
189
- extras: list[str],
190
- exception: Exception,
191
- ) -> SerializedError:
192
- return cls(
193
- type=type_,
194
- title=title,
195
- message=message,
196
- extras=extras,
197
- exception=format_exception(exception),
198
- exception_with_traceback=format_exception(exception, True),
199
- )
200
-
201
- @classmethod
202
- def from_exception(cls, exception: Exception) -> SerializedError:
203
- import requests
204
- import hypothesis.errors
205
- from hypothesis import HealthCheck
206
-
207
- title = "Runtime Error"
208
- message: str | None
209
- if isinstance(exception, requests.RequestException):
210
- if isinstance(exception, requests.exceptions.SSLError):
211
- type_ = RuntimeErrorType.CONNECTION_SSL
212
- elif isinstance(exception, requests.exceptions.ConnectionError):
213
- type_ = RuntimeErrorType.CONNECTION_OTHER
214
- else:
215
- type_ = RuntimeErrorType.NETWORK_OTHER
216
- message, extras = extract_requests_exception_details(exception)
217
- title = "Network Error"
218
- elif isinstance(exception, DeadlineExceeded):
219
- type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
220
- message = str(exception).strip()
221
- extras = []
222
- elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
223
- # Comes from `hypothesis-graphql`
224
- scalar_name = _scalar_name_from_error(exception)
225
- type_ = RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
226
- message = f"Scalar type '{scalar_name}' is not recognized"
227
- extras = []
228
- title = "Unknown GraphQL Scalar"
229
- elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).endswith(
230
- "larger than Hypothesis is designed to handle"
231
- ):
232
- type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
233
- message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
234
- extras = []
235
- title = "Failed Health Check"
236
- elif isinstance(exception, hypothesis.errors.Unsatisfiable):
237
- type_ = RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE
238
- message = f"{exception}. Possible reasons:"
239
- extras = [
240
- "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
241
- "- Excessive schema complexity, which hinders parameter generation.",
242
- ]
243
- title = "Schema Error"
244
- elif isinstance(exception, hypothesis.errors.FailedHealthCheck):
245
- health_check = _health_check_from_error(exception)
246
- if health_check is not None:
247
- message, type_ = {
248
- HealthCheck.data_too_large: (
249
- HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE,
250
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
251
- ),
252
- HealthCheck.filter_too_much: (
253
- HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH,
254
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
255
- ),
256
- HealthCheck.too_slow: (
257
- HEALTH_CHECK_MESSAGE_TOO_SLOW,
258
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
259
- ),
260
- HealthCheck.large_base_example: (
261
- HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE,
262
- RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
263
- ),
264
- }[health_check]
265
- else:
266
- type_ = RuntimeErrorType.UNCLASSIFIED
267
- message = str(exception)
268
- extras = []
269
- title = "Failed Health Check"
270
- elif isinstance(exception, OperationSchemaError):
271
- if isinstance(exception, BodyInGetRequestError):
272
- type_ = RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST
273
- elif isinstance(exception, InvalidRegularExpression) and exception.is_valid_type:
274
- type_ = RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION
275
- else:
276
- type_ = RuntimeErrorType.SCHEMA_GENERIC
277
- message = exception.message
278
- extras = []
279
- title = "Schema Error"
280
- elif isinstance(exception, SerializationError):
281
- if isinstance(exception, UnboundPrefixError):
282
- type_ = RuntimeErrorType.SERIALIZATION_UNBOUNDED_PREFIX
283
- title = "XML serialization error"
284
- else:
285
- title = "Serialization not possible"
286
- type_ = RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE
287
- message = str(exception)
288
- extras = []
289
- else:
290
- type_ = RuntimeErrorType.UNCLASSIFIED
291
- message = str(exception)
292
- extras = []
293
- return cls.with_exception(type_=type_, exception=exception, title=title, message=message, extras=extras)
294
-
295
-
296
- HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
297
- Typically, generating excessively large examples can compromise the quality of test outcomes.
298
-
299
- Consider revising the schema to more accurately represent typical use cases
300
- or applying constraints to reduce the data size."""
301
- HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
302
- that the schema's constraints may be too complex.
303
-
304
- This level of filtration can slow down testing and affect the distribution
305
- of generated data. Review and simplify the schema constraints where
306
- possible to mitigate this issue."""
307
- HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
308
- HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
309
- is excessively large, potentially leading to inefficient test execution.
310
-
311
- This is commonly due to schemas that specify large-scale data structures by
312
- default, such as an array with an extensive number of elements.
313
-
314
- Consider revising the schema to more accurately represent typical use cases
315
- or applying constraints to reduce the data size."""
316
-
317
-
318
- def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
319
- from hypothesis import HealthCheck
320
-
321
- match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(exception))
322
- if match:
323
- return {
324
- "data_too_large": HealthCheck.data_too_large,
325
- "filter_too_much": HealthCheck.filter_too_much,
326
- "too_slow": HealthCheck.too_slow,
327
- "large_base_example": HealthCheck.large_base_example,
328
- }.get(match.group(1))
329
- return None
330
-
331
-
332
- def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
333
- # This one is always available as the format is checked upfront
334
- match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
335
- match = cast(re.Match, match)
336
- return match.group(1)
337
-
338
-
339
- @dataclass
340
- class SerializedInteraction:
341
- request: Request
342
- response: Response
343
- checks: list[SerializedCheck]
344
- status: Status
345
- recorded_at: str
346
-
347
- @classmethod
348
- def from_interaction(cls, interaction: Interaction) -> SerializedInteraction:
349
- return cls(
350
- request=interaction.request,
351
- response=interaction.response,
352
- checks=[SerializedCheck.from_check(check) for check in interaction.checks],
353
- status=interaction.status,
354
- recorded_at=interaction.recorded_at,
355
- )
356
-
357
-
358
- @dataclass
359
- class SerializedTestResult:
360
- method: str
361
- path: str
362
- verbose_name: str
363
- has_failures: bool
364
- has_errors: bool
365
- has_logs: bool
366
- is_errored: bool
367
- is_flaky: bool
368
- is_skipped: bool
369
- skip_reason: str | None
370
- seed: int | None
371
- data_generation_method: list[str]
372
- checks: list[SerializedCheck]
373
- logs: list[str]
374
- errors: list[SerializedError]
375
- interactions: list[SerializedInteraction]
376
-
377
- @classmethod
378
- def from_test_result(cls, result: TestResult) -> SerializedTestResult:
379
- formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
380
- return cls(
381
- method=result.method,
382
- path=result.path,
383
- verbose_name=result.verbose_name,
384
- has_failures=result.has_failures,
385
- has_errors=result.has_errors,
386
- has_logs=result.has_logs,
387
- is_errored=result.is_errored,
388
- is_flaky=result.is_flaky,
389
- is_skipped=result.is_skipped,
390
- skip_reason=result.skip_reason,
391
- seed=result.seed,
392
- data_generation_method=[m.as_short_name() for m in result.data_generation_method],
393
- checks=[SerializedCheck.from_check(check) for check in result.checks],
394
- logs=[formatter.format(record) for record in result.logs],
395
- errors=[SerializedError.from_exception(error) for error in result.errors],
396
- interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
397
- )
398
-
399
-
400
- def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]:
401
- """Return only unique checks that should be displayed in the output."""
402
- seen: set[tuple[str | None, ...]] = set()
403
- unique_checks = []
404
- for check in reversed(checks):
405
- # There are also could be checks that didn't fail
406
- if check.value == Status.failure:
407
- key = make_unique_by_key(check.name, check.message, check.context)
408
- if key not in seen:
409
- unique_checks.append(check)
410
- seen.add(key)
411
- return unique_checks
@@ -1,248 +0,0 @@
1
- from __future__ import annotations
2
- import threading
3
- from collections.abc import MutableMapping, MutableSequence
4
- from dataclasses import dataclass, replace
5
- from typing import TYPE_CHECKING, Any, cast
6
- from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
7
-
8
- from .constants import NOT_SET
9
-
10
- if TYPE_CHECKING:
11
- from requests import PreparedRequest
12
- from .models import Case, CaseSource, Request
13
- from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
14
- from .transports.responses import GenericResponse
15
-
16
- # Exact keys to sanitize
17
- DEFAULT_KEYS_TO_SANITIZE = frozenset(
18
- (
19
- "phpsessid",
20
- "xsrf-token",
21
- "_csrf",
22
- "_csrf_token",
23
- "_session",
24
- "_xsrf",
25
- "aiohttp_session",
26
- "api_key",
27
- "api-key",
28
- "apikey",
29
- "auth",
30
- "authorization",
31
- "connect.sid",
32
- "cookie",
33
- "credentials",
34
- "csrf",
35
- "csrf_token",
36
- "csrf-token",
37
- "csrftoken",
38
- "ip_address",
39
- "mysql_pwd",
40
- "passwd",
41
- "password",
42
- "private_key",
43
- "private-key",
44
- "privatekey",
45
- "remote_addr",
46
- "remote-addr",
47
- "secret",
48
- "session",
49
- "sessionid",
50
- "set_cookie",
51
- "set-cookie",
52
- "token",
53
- "x_api_key",
54
- "x-api-key",
55
- "x_csrftoken",
56
- "x-csrftoken",
57
- "x_forwarded_for",
58
- "x-forwarded-for",
59
- "x_real_ip",
60
- "x-real-ip",
61
- )
62
- )
63
-
64
- # Markers indicating potentially sensitive keys
65
- DEFAULT_SENSITIVE_MARKERS = frozenset(
66
- (
67
- "token",
68
- "key",
69
- "secret",
70
- "password",
71
- "auth",
72
- "session",
73
- "passwd",
74
- "credential",
75
- )
76
- )
77
-
78
- DEFAULT_REPLACEMENT = "[Filtered]"
79
-
80
-
81
- @dataclass
82
- class Config:
83
- """Configuration class for sanitizing sensitive data.
84
-
85
- :param FrozenSet[str] keys_to_sanitize: The exact keys to sanitize (case-insensitive).
86
- :param FrozenSet[str] sensitive_markers: Markers indicating potentially sensitive keys (case-insensitive).
87
- :param str replacement: The replacement string for sanitized values.
88
- """
89
-
90
- keys_to_sanitize: frozenset[str] = DEFAULT_KEYS_TO_SANITIZE
91
- sensitive_markers: frozenset[str] = DEFAULT_SENSITIVE_MARKERS
92
- replacement: str = DEFAULT_REPLACEMENT
93
-
94
- def with_keys_to_sanitize(self, *keys: str) -> Config:
95
- """Create a new configuration with additional keys to sanitize."""
96
- new_keys_to_sanitize = self.keys_to_sanitize.union([key.lower() for key in keys])
97
- return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
98
-
99
- def without_keys_to_sanitize(self, *keys: str) -> Config:
100
- """Create a new configuration without certain keys to sanitize."""
101
- new_keys_to_sanitize = self.keys_to_sanitize.difference([key.lower() for key in keys])
102
- return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
103
-
104
- def with_sensitive_markers(self, *markers: str) -> Config:
105
- """Create a new configuration with additional sensitive markers."""
106
- new_sensitive_markers = self.sensitive_markers.union([key.lower() for key in markers])
107
- return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
108
-
109
- def without_sensitive_markers(self, *markers: str) -> Config:
110
- """Create a new configuration without certain sensitive markers."""
111
- new_sensitive_markers = self.sensitive_markers.difference([key.lower() for key in markers])
112
- return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
113
-
114
-
115
- _thread_local = threading.local()
116
-
117
-
118
- def _get_default_sanitization_config() -> Config:
119
- # Initialize the thread-local default sanitization config if not already set
120
- if not hasattr(_thread_local, "default_sanitization_config"):
121
- _thread_local.default_sanitization_config = Config()
122
- return _thread_local.default_sanitization_config
123
-
124
-
125
- def configure(config: Config) -> None:
126
- _thread_local.default_sanitization_config = config
127
-
128
-
129
- def sanitize_value(item: Any, *, config: Config | None = None) -> None:
130
- """Sanitize sensitive values within a given item.
131
-
132
- This function is recursive and will sanitize sensitive data within nested
133
- dictionaries and lists as well.
134
- """
135
- config = config or _get_default_sanitization_config()
136
- if isinstance(item, MutableMapping):
137
- for key in list(item.keys()):
138
- lower_key = key.lower()
139
- if lower_key in config.keys_to_sanitize or any(marker in lower_key for marker in config.sensitive_markers):
140
- if isinstance(item[key], list):
141
- item[key] = [config.replacement]
142
- else:
143
- item[key] = config.replacement
144
- for value in item.values():
145
- if isinstance(value, (MutableMapping, MutableSequence)):
146
- sanitize_value(value, config=config)
147
- elif isinstance(item, MutableSequence):
148
- for value in item:
149
- if isinstance(value, (MutableMapping, MutableSequence)):
150
- sanitize_value(value, config=config)
151
-
152
-
153
- def sanitize_case(case: Case, *, config: Config | None = None) -> None:
154
- """Sanitize sensitive values within a given case."""
155
- if case.path_parameters is not None:
156
- sanitize_value(case.path_parameters, config=config)
157
- if case.headers is not None:
158
- sanitize_value(case.headers, config=config)
159
- if case.cookies is not None:
160
- sanitize_value(case.cookies, config=config)
161
- if case.query is not None:
162
- sanitize_value(case.query, config=config)
163
- if case.body not in (None, NOT_SET):
164
- sanitize_value(case.body, config=config)
165
- if case.source is not None:
166
- sanitize_history(case.source, config=config)
167
-
168
-
169
- def sanitize_history(source: CaseSource, *, config: Config | None = None) -> None:
170
- """Recursively sanitize history of case/response pairs."""
171
- current: CaseSource | None = source
172
- while current is not None:
173
- sanitize_case(current.case, config=config)
174
- sanitize_response(current.response, config=config)
175
- current = current.case.source
176
-
177
-
178
- def sanitize_response(response: GenericResponse, *, config: Config | None = None) -> None:
179
- # Sanitize headers
180
- sanitize_value(response.headers, config=config)
181
-
182
-
183
- def sanitize_request(request: PreparedRequest | Request, *, config: Config | None = None) -> None:
184
- from requests import PreparedRequest
185
-
186
- if isinstance(request, PreparedRequest) and request.url:
187
- request.url = sanitize_url(request.url, config=config)
188
- else:
189
- request = cast("Request", request)
190
- request.uri = sanitize_url(request.uri, config=config)
191
- # Sanitize headers
192
- sanitize_value(request.headers, config=config)
193
-
194
-
195
- def sanitize_output(case: Case, response: GenericResponse | None = None, *, config: Config | None = None) -> None:
196
- sanitize_case(case, config=config)
197
- if response is not None:
198
- sanitize_response(response, config=config)
199
- sanitize_request(response.request, config=config)
200
-
201
-
202
- def sanitize_url(url: str, *, config: Config | None = None) -> str:
203
- """Sanitize sensitive parts of a given URL.
204
-
205
- This function will sanitize the authority and query parameters in the URL.
206
- """
207
- config = config or _get_default_sanitization_config()
208
- parsed = urlsplit(url)
209
-
210
- # Sanitize authority
211
- netloc_parts = parsed.netloc.split("@")
212
- if len(netloc_parts) > 1:
213
- netloc = f"{config.replacement}@{netloc_parts[-1]}"
214
- else:
215
- netloc = parsed.netloc
216
-
217
- # Sanitize query parameters
218
- query = parse_qs(parsed.query, keep_blank_values=True)
219
- sanitize_value(query, config=config)
220
- sanitized_query = urlencode(query, doseq=True)
221
-
222
- # Reconstruct the URL
223
- sanitized_url_parts = parsed._replace(netloc=netloc, query=sanitized_query)
224
- return urlunsplit(sanitized_url_parts)
225
-
226
-
227
- def sanitize_serialized_check(check: SerializedCheck, *, config: Config | None = None) -> None:
228
- sanitize_request(check.request, config=config)
229
- response = check.response
230
- if response:
231
- sanitize_value(response.headers, config=config)
232
- sanitize_serialized_case(check.example, config=config)
233
- for entry in check.history:
234
- sanitize_serialized_case(entry.case, config=config)
235
- sanitize_value(entry.response.headers, config=config)
236
-
237
-
238
- def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = None) -> None:
239
- for value in (case.path_parameters, case.headers, case.cookies, case.query, case.extra_headers):
240
- if value is not None:
241
- sanitize_value(value, config=config)
242
-
243
-
244
- def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
245
- sanitize_request(interaction.request, config=config)
246
- sanitize_value(interaction.response.headers, config=config)
247
- for check in interaction.checks:
248
- sanitize_serialized_check(check, config=config)