schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,82 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
-
5
- from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
6
- from ._lazy_import import lazy_import
7
- from .constants import SCHEMATHESIS_VERSION
8
- from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
9
- from .models import Case
10
- from .specs import openapi
3
+ from schemathesis import auths, contrib, engine, errors, experimental, graphql, hooks, openapi, pytest, python
4
+ from schemathesis.checks import CheckContext, CheckFunction, check
5
+ from schemathesis.core.output import OutputConfig, sanitization
6
+ from schemathesis.core.transport import Response
7
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
8
+ from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
9
+ from schemathesis.generation.case import Case
10
+ from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
11
 
12
12
  __version__ = SCHEMATHESIS_VERSION
13
13
 
14
- # Default loaders
15
- from_aiohttp = openapi.from_aiohttp
16
- from_asgi = openapi.from_asgi
17
- from_dict = openapi.from_dict
18
- from_file = openapi.from_file
19
- from_path = openapi.from_path
20
- from_pytest_fixture = openapi.from_pytest_fixture
21
- from_uri = openapi.from_uri
22
- from_wsgi = openapi.from_wsgi
23
-
24
14
  # Public API
25
15
  auth = auths.GLOBAL_AUTH_STORAGE
26
- check = checks.register
27
16
  hook = hooks.register
28
- serializer = serializers.register
29
- target = targets.register
30
-
31
- # Backward compatibility
32
- register_check = checks.register
33
- register_target = targets.register
34
- register_string_format = openapi.format
35
17
 
36
18
  __all__ = [
37
- "auths",
38
- "checks",
39
- "experimental",
40
- "contrib",
41
- "fixups",
42
- "graphql",
43
- "hooks",
44
- "runner",
45
- "serializers",
46
- "targets",
47
- "DataGenerationMethod",
48
- "SCHEMATHESIS_VERSION",
49
19
  "Case",
50
- "openapi",
20
+ "CheckContext",
21
+ "CheckFunction",
22
+ "GenerationMode",
23
+ "GenerationConfig",
24
+ "HeaderConfig",
25
+ "OutputConfig",
26
+ "Response",
27
+ "TargetContext",
28
+ "TargetFunction",
51
29
  "__version__",
52
- "from_aiohttp",
53
- "from_asgi",
54
- "from_dict",
55
- "from_file",
56
- "from_path",
57
- "from_pytest_fixture",
58
- "from_uri",
59
- "from_wsgi",
60
30
  "auth",
61
31
  "check",
32
+ "contrib",
33
+ "engine",
34
+ "errors",
35
+ "experimental",
36
+ "graphql",
62
37
  "hook",
63
- "serializer",
38
+ "hooks",
39
+ "openapi",
40
+ "pytest",
41
+ "python",
42
+ "sanitization",
64
43
  "target",
65
- "register_check",
66
- "register_target",
67
- "register_string_format",
68
44
  ]
69
-
70
-
71
- def _load_generic_response() -> Any:
72
- from .transports.responses import GenericResponse
73
-
74
- return GenericResponse
75
-
76
-
77
- _imports = {"GenericResponse": _load_generic_response}
78
-
79
-
80
- def __getattr__(name: str) -> Any:
81
- # Some modules are relatively heavy, hence load them lazily to improve startup time for CLI
82
- return lazy_import(__name__, name, _imports, globals())
schemathesis/auths.py CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import inspect
6
5
  import threading
7
6
  import time
8
- import warnings
9
7
  from dataclasses import dataclass, field
10
8
  from typing import (
11
9
  TYPE_CHECKING,
@@ -19,17 +17,18 @@ from typing import (
19
17
  runtime_checkable,
20
18
  )
21
19
 
22
- from .exceptions import UsageError
23
- from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
20
+ from schemathesis.core.errors import IncorrectUsage
21
+ from schemathesis.core.marks import Mark
22
+ from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
23
+ from schemathesis.generation.case import Case
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  import requests.auth
27
27
 
28
- from .models import APIOperation, Case
29
- from .types import GenericTest
28
+ from schemathesis.schemas import APIOperation
30
29
 
31
30
  DEFAULT_REFRESH_INTERVAL = 300
32
- AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
31
+ AuthStorageMark = Mark["AuthStorage"](attr_name="auth_storage")
33
32
  Auth = TypeVar("Auth")
34
33
 
35
34
 
@@ -111,7 +110,7 @@ class CachingAuthProvider(Generic[Auth]):
111
110
  # Another thread updated the cache
112
111
  return cache_entry.data
113
112
  # We know that optional auth is possible only inside a higher-level wrapper
114
- data: Auth = _provider_get(self.provider, case, context) # type: ignore[assignment]
113
+ data: Auth = self.provider.get(case, context) # type: ignore[assignment]
115
114
  self._set_cache_entry(data, case, context)
116
115
  return data
117
116
  return cache_entry.data
@@ -152,8 +151,7 @@ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
152
151
  class FilterableRegisterAuth(Protocol):
153
152
  """Protocol that adds filters to the return value of `register`."""
154
153
 
155
- def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
156
- pass
154
+ def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
157
155
 
158
156
  def apply_to(
159
157
  self,
@@ -165,8 +163,7 @@ class FilterableRegisterAuth(Protocol):
165
163
  method_regex: str | None = None,
166
164
  path: FilterValue | None = None,
167
165
  path_regex: str | None = None,
168
- ) -> FilterableRegisterAuth:
169
- pass
166
+ ) -> FilterableRegisterAuth: ...
170
167
 
171
168
  def skip_for(
172
169
  self,
@@ -178,15 +175,13 @@ class FilterableRegisterAuth(Protocol):
178
175
  method_regex: str | None = None,
179
176
  path: FilterValue | None = None,
180
177
  path_regex: str | None = None,
181
- ) -> FilterableRegisterAuth:
182
- pass
178
+ ) -> FilterableRegisterAuth: ...
183
179
 
184
180
 
185
181
  class FilterableApplyAuth(Protocol):
186
182
  """Protocol that adds filters to the return value of `apply`."""
187
183
 
188
- def __call__(self, test: GenericTest) -> GenericTest:
189
- pass
184
+ def __call__(self, test: Callable) -> Callable: ...
190
185
 
191
186
  def apply_to(
192
187
  self,
@@ -198,8 +193,7 @@ class FilterableApplyAuth(Protocol):
198
193
  method_regex: str | None = None,
199
194
  path: FilterValue | None = None,
200
195
  path_regex: str | None = None,
201
- ) -> FilterableApplyAuth:
202
- pass
196
+ ) -> FilterableApplyAuth: ...
203
197
 
204
198
  def skip_for(
205
199
  self,
@@ -211,8 +205,7 @@ class FilterableApplyAuth(Protocol):
211
205
  method_regex: str | None = None,
212
206
  path: FilterValue | None = None,
213
207
  path_regex: str | None = None,
214
- ) -> FilterableApplyAuth:
215
- pass
208
+ ) -> FilterableApplyAuth: ...
216
209
 
217
210
 
218
211
  class FilterableRequestsAuth(Protocol):
@@ -228,8 +221,7 @@ class FilterableRequestsAuth(Protocol):
228
221
  method_regex: str | None = None,
229
222
  path: FilterValue | None = None,
230
223
  path_regex: str | None = None,
231
- ) -> FilterableRequestsAuth:
232
- pass
224
+ ) -> FilterableRequestsAuth: ...
233
225
 
234
226
  def skip_for(
235
227
  self,
@@ -241,8 +233,7 @@ class FilterableRequestsAuth(Protocol):
241
233
  method_regex: str | None = None,
242
234
  path: FilterValue | None = None,
243
235
  path_regex: str | None = None,
244
- ) -> FilterableRequestsAuth:
245
- pass
236
+ ) -> FilterableRequestsAuth: ...
246
237
 
247
238
 
248
239
  @dataclass
@@ -254,7 +245,7 @@ class SelectiveAuthProvider(Generic[Auth]):
254
245
 
255
246
  def get(self, case: Case, context: AuthContext) -> Auth | None:
256
247
  if self.filter_set.match(context):
257
- return _provider_get(self.provider, case, context)
248
+ return self.provider.get(case, context)
258
249
  return None
259
250
 
260
251
  def set(self, case: Case, data: Auth, context: AuthContext) -> None:
@@ -278,8 +269,7 @@ class AuthStorage(Generic[Auth]):
278
269
  *,
279
270
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
280
271
  cache_by_key: CacheKeyFunction | None = None,
281
- ) -> FilterableRegisterAuth:
282
- pass
272
+ ) -> FilterableRegisterAuth: ...
283
273
 
284
274
  @overload
285
275
  def __call__(
@@ -288,8 +278,7 @@ class AuthStorage(Generic[Auth]):
288
278
  *,
289
279
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
290
280
  cache_by_key: CacheKeyFunction | None = None,
291
- ) -> FilterableApplyAuth:
292
- pass
281
+ ) -> FilterableApplyAuth: ...
293
282
 
294
283
  def __call__(
295
284
  self,
@@ -307,8 +296,7 @@ class AuthStorage(Generic[Auth]):
307
296
  filter_set = FilterSet()
308
297
  self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
309
298
 
310
- class _FilterableRequestsAuth:
311
- pass
299
+ class _FilterableRequestsAuth: ...
312
300
 
313
301
  attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
314
302
  attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
@@ -418,8 +406,11 @@ class AuthStorage(Generic[Auth]):
418
406
  """
419
407
  filter_set = FilterSet()
420
408
 
421
- def wrapper(test: GenericTest) -> GenericTest:
422
- auth_storage = self.add_auth_storage(test)
409
+ def wrapper(test: Callable) -> Callable:
410
+ if AuthStorageMark.is_set(test):
411
+ raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `apply`.")
412
+ auth_storage = self.__class__()
413
+ AuthStorageMark.set(test, auth_storage)
423
414
  auth_storage._set_provider(
424
415
  provider_class=provider_class,
425
416
  refresh_interval=refresh_interval,
@@ -433,46 +424,18 @@ class AuthStorage(Generic[Auth]):
433
424
 
434
425
  return wrapper # type: ignore[return-value]
435
426
 
436
- @classmethod
437
- def add_auth_storage(cls, test: GenericTest) -> AuthStorage:
438
- """Attach a new auth storage instance to the test if it is not already present."""
439
- if not hasattr(test, AUTH_STORAGE_ATTRIBUTE_NAME):
440
- setattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, cls())
441
- else:
442
- raise UsageError(f"`{test.__name__}` has already been decorated with `apply`.")
443
- return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME)
444
-
445
427
  def set(self, case: Case, context: AuthContext) -> None:
446
428
  """Set authentication data on a generated test case."""
447
429
  if not self.is_defined:
448
- raise UsageError("No auth provider is defined.")
430
+ raise IncorrectUsage("No auth provider is defined.")
449
431
  for provider in self.providers:
450
- data: Auth | None = _provider_get(provider, case, context)
432
+ data: Auth | None = provider.get(case, context)
451
433
  if data is not None:
452
434
  provider.set(case, data, context)
453
435
  case._has_explicit_auth = True
454
436
  break
455
437
 
456
438
 
457
- def _provider_get(auth_provider: AuthProvider, case: Case, context: AuthContext) -> Auth | None:
458
- # A shim to provide a compatibility layer between previously used convention for `AuthProvider.get`
459
- # where it used to accept a single `context` argument
460
- method = auth_provider.get
461
- parameters = inspect.signature(method).parameters
462
- if len(parameters) == 1:
463
- # Old calling convention
464
- warnings.warn(
465
- "The method 'get' of your AuthProvider is using the old calling convention, "
466
- "which is deprecated and will be removed in Schemathesis 4.0. "
467
- "Please update it to accept both 'case' and 'context' as arguments.",
468
- DeprecationWarning,
469
- stacklevel=1,
470
- )
471
- return method(context) # type: ignore
472
- # New calling convention
473
- return method(case, context)
474
-
475
-
476
439
  def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
477
440
  """Set authentication data on this case.
478
441
 
@@ -486,11 +449,6 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
486
449
  GLOBAL_AUTH_STORAGE.set(case, context)
487
450
 
488
451
 
489
- def get_auth_storage_from_test(test: GenericTest) -> AuthStorage | None:
490
- """Extract the currently attached auth storage from a test function."""
491
- return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, None)
492
-
493
-
494
452
  # Global auth API
495
453
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
496
454
  register = GLOBAL_AUTH_STORAGE.register
schemathesis/checks.py CHANGED
@@ -1,80 +1,150 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import TYPE_CHECKING
5
-
6
- from . import failures
7
- from .exceptions import get_response_parsing_error, get_server_error
8
- from .specs.openapi.checks import (
9
- content_type_conformance,
10
- ignored_auth,
11
- negative_data_rejection,
12
- response_headers_conformance,
13
- response_schema_conformance,
14
- status_code_conformance,
4
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
5
+
6
+ from schemathesis.core.failures import (
7
+ CustomFailure,
8
+ Failure,
9
+ FailureGroup,
10
+ MalformedJson,
11
+ MaxResponseTimeConfig,
12
+ ResponseTimeExceeded,
13
+ ServerError,
15
14
  )
15
+ from schemathesis.core.registries import Registry
16
+ from schemathesis.core.transport import Response
17
+ from schemathesis.generation.overrides import Override
16
18
 
17
19
  if TYPE_CHECKING:
18
- from .internal.checks import CheckContext, CheckFunction
19
- from .models import Case
20
- from .transports.responses import GenericResponse
20
+ from requests.models import CaseInsensitiveDict
21
21
 
22
+ from schemathesis.engine.recorder import ScenarioRecorder
23
+ from schemathesis.generation.case import Case
22
24
 
23
- def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
25
+ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
26
+ ChecksConfig = dict[CheckFunction, Any]
27
+
28
+
29
+ class CheckContext:
30
+ """Context for Schemathesis checks.
31
+
32
+ Provides access to broader test execution data beyond individual test cases.
33
+ """
34
+
35
+ override: Override | None
36
+ auth: tuple[str, str] | None
37
+ headers: CaseInsensitiveDict | None
38
+ config: ChecksConfig
39
+ transport_kwargs: dict[str, Any] | None
40
+ recorder: ScenarioRecorder | None
41
+
42
+ __slots__ = ("override", "auth", "headers", "config", "transport_kwargs", "recorder")
43
+
44
+ def __init__(
45
+ self,
46
+ override: Override | None,
47
+ auth: tuple[str, str] | None,
48
+ headers: CaseInsensitiveDict | None,
49
+ config: ChecksConfig,
50
+ transport_kwargs: dict[str, Any] | None,
51
+ recorder: ScenarioRecorder | None = None,
52
+ ) -> None:
53
+ self.override = override
54
+ self.auth = auth
55
+ self.headers = headers
56
+ self.config = config
57
+ self.transport_kwargs = transport_kwargs
58
+ self.recorder = recorder
59
+
60
+ def find_parent(self, *, case_id: str) -> Case | None:
61
+ if self.recorder is not None:
62
+ return self.recorder.find_parent(case_id=case_id)
63
+ return None
64
+
65
+ def find_related(self, *, case_id: str) -> Iterator[Case]:
66
+ if self.recorder is not None:
67
+ yield from self.recorder.find_related(case_id=case_id)
68
+
69
+ def find_response(self, *, case_id: str) -> Response | None:
70
+ if self.recorder is not None:
71
+ return self.recorder.find_response(case_id=case_id)
72
+ return None
73
+
74
+ def record_case(self, *, parent_id: str, case: Case) -> None:
75
+ if self.recorder is not None:
76
+ self.recorder.record_case(parent_id=parent_id, transition=None, case=case)
77
+
78
+ def record_response(self, *, case_id: str, response: Response) -> None:
79
+ if self.recorder is not None:
80
+ self.recorder.record_response(case_id=case_id, response=response)
81
+
82
+
83
+ CHECKS = Registry[CheckFunction]()
84
+ check = CHECKS.register
85
+
86
+
87
+ @check
88
+ def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
24
89
  """A check to verify that the response is not a server-side error."""
25
- from .specs.graphql.schemas import GraphQLCase
90
+ from .specs.graphql.schemas import GraphQLSchema
26
91
  from .specs.graphql.validation import validate_graphql_response
27
- from .transports.responses import get_json
28
92
 
29
93
  status_code = response.status_code
30
94
  if status_code >= 500:
31
- exc_class = get_server_error(case.operation.verbose_name, status_code)
32
- raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
33
- if isinstance(case, GraphQLCase):
95
+ raise ServerError(operation=case.operation.label, status_code=status_code)
96
+ if isinstance(case.operation.schema, GraphQLSchema):
34
97
  try:
35
- data = get_json(response)
36
- validate_graphql_response(data)
98
+ data = response.json()
99
+ validate_graphql_response(case, data)
37
100
  except json.JSONDecodeError as exc:
38
- exc_class = get_response_parsing_error(case.operation.verbose_name, exc)
39
- context = failures.JSONDecodeErrorContext.from_exception(exc)
40
- raise exc_class(context.title, context=context) from exc
101
+ raise MalformedJson.from_exception(operation=case.operation.label, exc=exc) from None
41
102
  return None
42
103
 
43
104
 
44
- def _make_max_response_time_failure_message(elapsed_time: float, max_response_time: int) -> str:
45
- return f"Actual: {elapsed_time:.2f}ms\nLimit: {max_response_time}.00ms"
46
-
47
-
48
- DEFAULT_CHECKS: tuple[CheckFunction, ...] = (not_a_server_error,)
49
- OPTIONAL_CHECKS = (
50
- status_code_conformance,
51
- content_type_conformance,
52
- response_headers_conformance,
53
- response_schema_conformance,
54
- negative_data_rejection,
55
- ignored_auth,
56
- )
57
- ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
58
-
59
-
60
- def register(check: CheckFunction) -> CheckFunction:
61
- """Register a new check for schemathesis CLI.
62
-
63
- :param check: A function to validate API responses.
64
-
65
- .. code-block:: python
66
-
67
- @schemathesis.check
68
- def new_check(ctx, response, case):
69
- # some awesome assertions!
70
- ...
71
- """
72
- from . import cli
73
- from .internal.checks import wrap_check
105
+ def max_response_time(ctx: CheckContext, response: Response, case: Case) -> bool | None:
106
+ config = ctx.config.get(max_response_time, MaxResponseTimeConfig())
107
+ elapsed = response.elapsed
108
+ if elapsed > config.limit:
109
+ raise ResponseTimeExceeded(
110
+ operation=case.operation.label,
111
+ message=f"Actual: {elapsed:.2f}ms\nLimit: {config.limit * 1000:.2f}ms",
112
+ elapsed=elapsed,
113
+ deadline=config.limit,
114
+ )
115
+ return None
74
116
 
75
- _check = wrap_check(check)
76
- global ALL_CHECKS
77
117
 
78
- ALL_CHECKS += (_check,)
79
- cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
80
- return check
118
+ def run_checks(
119
+ *,
120
+ case: Case,
121
+ response: Response,
122
+ ctx: CheckContext,
123
+ checks: Iterable[CheckFunction],
124
+ on_failure: Callable[[str, set[Failure], Failure], None],
125
+ on_success: Callable[[str, Case], None] | None = None,
126
+ ) -> set[Failure]:
127
+ """Run a set of checks against a response."""
128
+ collected: set[Failure] = set()
129
+
130
+ for check in checks:
131
+ name = check.__name__
132
+ try:
133
+ skip_check = check(ctx, response, case)
134
+ if not skip_check and on_success:
135
+ on_success(name, case)
136
+ except Failure as failure:
137
+ on_failure(name, collected, failure.with_traceback(None))
138
+ except AssertionError as exc:
139
+ custom_failure = CustomFailure(
140
+ operation=case.operation.label,
141
+ title=f"Custom check failed: `{name}`",
142
+ message=str(exc),
143
+ exception=exc,
144
+ )
145
+ on_failure(name, collected, custom_failure)
146
+ except FailureGroup as group:
147
+ for sub_failure in group.exceptions:
148
+ on_failure(name, collected, sub_failure)
149
+
150
+ return collected