schemathesis 3.25.5__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 -1766
  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/{cli → engine/phases}/probes.py +63 -70
  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 +153 -39
  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 +483 -367
  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.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.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 -55
  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 -765
  156. schemathesis/cli/output/short.py +0 -40
  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 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  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 -315
  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 -184
  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.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,82 +1,44 @@
1
1
  from __future__ import annotations
2
- from typing import Any
3
-
4
- from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
- from ._lazy_import import lazy_import
6
- from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
7
- from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
- from .models import Case # noqa: E402
9
- from .specs import openapi # noqa: E402
10
2
 
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
@@ -1,31 +1,34 @@
1
1
  """Support for custom API authentication mechanisms."""
2
+
2
3
  from __future__ import annotations
3
- import inspect
4
+
4
5
  import threading
5
6
  import time
6
- import warnings
7
7
  from dataclasses import dataclass, field
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
10
  Any,
11
11
  Callable,
12
12
  Generic,
13
+ Protocol,
13
14
  TypeVar,
15
+ Union,
14
16
  overload,
15
17
  runtime_checkable,
16
- Protocol,
17
18
  )
18
19
 
19
- from .exceptions import UsageError
20
- from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
21
- from .types import GenericTest
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
22
24
 
23
25
  if TYPE_CHECKING:
24
- from .models import APIOperation, Case
25
26
  import requests.auth
26
27
 
28
+ from schemathesis.schemas import APIOperation
29
+
27
30
  DEFAULT_REFRESH_INTERVAL = 300
28
- AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
31
+ AuthStorageMark = Mark["AuthStorage"](attr_name="auth_storage")
29
32
  Auth = TypeVar("Auth")
30
33
 
31
34
 
@@ -41,6 +44,9 @@ class AuthContext:
41
44
  app: Any | None
42
45
 
43
46
 
47
+ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
48
+
49
+
44
50
  @runtime_checkable
45
51
  class AuthProvider(Generic[Auth], Protocol):
46
52
  """Get authentication data for an API and set it on the generated test cases."""
@@ -96,16 +102,24 @@ class CachingAuthProvider(Generic[Auth]):
96
102
 
97
103
  def get(self, case: Case, context: AuthContext) -> Auth | None:
98
104
  """Get cached auth value."""
99
- if self.cache_entry is None or self.timer() >= self.cache_entry.expires:
105
+ cache_entry = self._get_cache_entry(case, context)
106
+ if cache_entry is None or self.timer() >= cache_entry.expires:
100
107
  with self._refresh_lock:
101
- if not (self.cache_entry is None or self.timer() >= self.cache_entry.expires):
108
+ cache_entry = self._get_cache_entry(case, context)
109
+ if not (cache_entry is None or self.timer() >= cache_entry.expires):
102
110
  # Another thread updated the cache
103
- return self.cache_entry.data
111
+ return cache_entry.data
104
112
  # We know that optional auth is possible only inside a higher-level wrapper
105
- data: Auth = _provider_get(self.provider, case, context) # type: ignore[assignment]
106
- self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
113
+ data: Auth = self.provider.get(case, context) # type: ignore[assignment]
114
+ self._set_cache_entry(data, case, context)
107
115
  return data
108
- return self.cache_entry.data
116
+ return cache_entry.data
117
+
118
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
119
+ return self.cache_entry
120
+
121
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
122
+ self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
109
123
 
110
124
  def set(self, case: Case, data: Auth, context: AuthContext) -> None:
111
125
  """Set auth data on the `Case` instance.
@@ -115,11 +129,29 @@ class CachingAuthProvider(Generic[Auth]):
115
129
  self.provider.set(case, data, context)
116
130
 
117
131
 
132
+ def _noop_key_function(case: Case, context: AuthContext) -> str:
133
+ # Never used
134
+ raise NotImplementedError
135
+
136
+
137
+ @dataclass
138
+ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
139
+ cache_by_key: CacheKeyFunction = _noop_key_function
140
+ cache_entries: dict[str | int, CacheEntry[Auth] | None] = field(default_factory=dict)
141
+
142
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
143
+ key = self.cache_by_key(case, context)
144
+ return self.cache_entries.get(key)
145
+
146
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
147
+ key = self.cache_by_key(case, context)
148
+ self.cache_entries[key] = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
149
+
150
+
118
151
  class FilterableRegisterAuth(Protocol):
119
152
  """Protocol that adds filters to the return value of `register`."""
120
153
 
121
- def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
122
- pass
154
+ def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
123
155
 
124
156
  def apply_to(
125
157
  self,
@@ -131,8 +163,7 @@ class FilterableRegisterAuth(Protocol):
131
163
  method_regex: str | None = None,
132
164
  path: FilterValue | None = None,
133
165
  path_regex: str | None = None,
134
- ) -> FilterableRegisterAuth:
135
- pass
166
+ ) -> FilterableRegisterAuth: ...
136
167
 
137
168
  def skip_for(
138
169
  self,
@@ -144,15 +175,13 @@ class FilterableRegisterAuth(Protocol):
144
175
  method_regex: str | None = None,
145
176
  path: FilterValue | None = None,
146
177
  path_regex: str | None = None,
147
- ) -> FilterableRegisterAuth:
148
- pass
178
+ ) -> FilterableRegisterAuth: ...
149
179
 
150
180
 
151
181
  class FilterableApplyAuth(Protocol):
152
182
  """Protocol that adds filters to the return value of `apply`."""
153
183
 
154
- def __call__(self, test: GenericTest) -> GenericTest:
155
- pass
184
+ def __call__(self, test: Callable) -> Callable: ...
156
185
 
157
186
  def apply_to(
158
187
  self,
@@ -164,8 +193,7 @@ class FilterableApplyAuth(Protocol):
164
193
  method_regex: str | None = None,
165
194
  path: FilterValue | None = None,
166
195
  path_regex: str | None = None,
167
- ) -> FilterableApplyAuth:
168
- pass
196
+ ) -> FilterableApplyAuth: ...
169
197
 
170
198
  def skip_for(
171
199
  self,
@@ -177,8 +205,7 @@ class FilterableApplyAuth(Protocol):
177
205
  method_regex: str | None = None,
178
206
  path: FilterValue | None = None,
179
207
  path_regex: str | None = None,
180
- ) -> FilterableApplyAuth:
181
- pass
208
+ ) -> FilterableApplyAuth: ...
182
209
 
183
210
 
184
211
  class FilterableRequestsAuth(Protocol):
@@ -194,8 +221,7 @@ class FilterableRequestsAuth(Protocol):
194
221
  method_regex: str | None = None,
195
222
  path: FilterValue | None = None,
196
223
  path_regex: str | None = None,
197
- ) -> FilterableRequestsAuth:
198
- pass
224
+ ) -> FilterableRequestsAuth: ...
199
225
 
200
226
  def skip_for(
201
227
  self,
@@ -207,8 +233,7 @@ class FilterableRequestsAuth(Protocol):
207
233
  method_regex: str | None = None,
208
234
  path: FilterValue | None = None,
209
235
  path_regex: str | None = None,
210
- ) -> FilterableRequestsAuth:
211
- pass
236
+ ) -> FilterableRequestsAuth: ...
212
237
 
213
238
 
214
239
  @dataclass
@@ -220,7 +245,7 @@ class SelectiveAuthProvider(Generic[Auth]):
220
245
 
221
246
  def get(self, case: Case, context: AuthContext) -> Auth | None:
222
247
  if self.filter_set.match(context):
223
- return _provider_get(self.provider, case, context)
248
+ return self.provider.get(case, context)
224
249
  return None
225
250
 
226
251
  def set(self, case: Case, data: Auth, context: AuthContext) -> None:
@@ -243,8 +268,8 @@ class AuthStorage(Generic[Auth]):
243
268
  self,
244
269
  *,
245
270
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
246
- ) -> FilterableRegisterAuth:
247
- pass
271
+ cache_by_key: CacheKeyFunction | None = None,
272
+ ) -> FilterableRegisterAuth: ...
248
273
 
249
274
  @overload
250
275
  def __call__(
@@ -252,26 +277,26 @@ class AuthStorage(Generic[Auth]):
252
277
  provider_class: type[AuthProvider],
253
278
  *,
254
279
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
255
- ) -> FilterableApplyAuth:
256
- pass
280
+ cache_by_key: CacheKeyFunction | None = None,
281
+ ) -> FilterableApplyAuth: ...
257
282
 
258
283
  def __call__(
259
284
  self,
260
285
  provider_class: type[AuthProvider] | None = None,
261
286
  *,
262
287
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
288
+ cache_by_key: CacheKeyFunction | None = None,
263
289
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
264
290
  if provider_class is not None:
265
- return self.apply(provider_class, refresh_interval=refresh_interval)
266
- return self.register(refresh_interval=refresh_interval)
291
+ return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
292
+ return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
267
293
 
268
294
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
269
295
  """Use `requests` auth instance as an auth provider."""
270
296
  filter_set = FilterSet()
271
297
  self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
272
298
 
273
- class _FilterableRequestsAuth:
274
- pass
299
+ class _FilterableRequestsAuth: ...
275
300
 
276
301
  attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
277
302
  attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
@@ -283,6 +308,7 @@ class AuthStorage(Generic[Auth]):
283
308
  *,
284
309
  provider_class: type[AuthProvider],
285
310
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
311
+ cache_by_key: CacheKeyFunction | None = None,
286
312
  filter_set: FilterSet,
287
313
  ) -> None:
288
314
  if not issubclass(provider_class, AuthProvider):
@@ -292,16 +318,27 @@ class AuthStorage(Generic[Auth]):
292
318
  )
293
319
  provider: AuthProvider
294
320
  # Apply caching if desired
321
+ instance = provider_class()
295
322
  if refresh_interval is not None:
296
- provider = CachingAuthProvider(provider_class(), refresh_interval=refresh_interval)
323
+ if cache_by_key is None:
324
+ provider = CachingAuthProvider(instance, refresh_interval=refresh_interval)
325
+ else:
326
+ provider = KeyedCachingAuthProvider(
327
+ instance, refresh_interval=refresh_interval, cache_by_key=cache_by_key
328
+ )
297
329
  else:
298
- provider = provider_class()
330
+ provider = instance
299
331
  # Store filters if any
300
332
  if not filter_set.is_empty():
301
333
  provider = SelectiveAuthProvider(provider, filter_set)
302
334
  self.providers.append(provider)
303
335
 
304
- def register(self, *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL) -> FilterableRegisterAuth:
336
+ def register(
337
+ self,
338
+ *,
339
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
340
+ cache_by_key: CacheKeyFunction | None = None,
341
+ ) -> FilterableRegisterAuth:
305
342
  """Register a new auth provider.
306
343
 
307
344
  .. code-block:: python
@@ -323,7 +360,12 @@ class AuthStorage(Generic[Auth]):
323
360
  filter_set = FilterSet()
324
361
 
325
362
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
326
- self._set_provider(provider_class=provider_class, refresh_interval=refresh_interval, filter_set=filter_set)
363
+ self._set_provider(
364
+ provider_class=provider_class,
365
+ refresh_interval=refresh_interval,
366
+ filter_set=filter_set,
367
+ cache_by_key=cache_by_key,
368
+ )
327
369
  return provider_class
328
370
 
329
371
  attach_filter_chain(wrapper, "apply_to", filter_set.include)
@@ -339,7 +381,11 @@ class AuthStorage(Generic[Auth]):
339
381
  self.providers = []
340
382
 
341
383
  def apply(
342
- self, provider_class: type[AuthProvider], *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL
384
+ self,
385
+ provider_class: type[AuthProvider],
386
+ *,
387
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
388
+ cache_by_key: CacheKeyFunction | None = None,
343
389
  ) -> FilterableApplyAuth:
344
390
  """Register auth provider only on one test function.
345
391
 
@@ -360,10 +406,16 @@ class AuthStorage(Generic[Auth]):
360
406
  """
361
407
  filter_set = FilterSet()
362
408
 
363
- def wrapper(test: GenericTest) -> GenericTest:
364
- 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)
365
414
  auth_storage._set_provider(
366
- provider_class=provider_class, refresh_interval=refresh_interval, filter_set=filter_set
415
+ provider_class=provider_class,
416
+ refresh_interval=refresh_interval,
417
+ filter_set=filter_set,
418
+ cache_by_key=cache_by_key,
367
419
  )
368
420
  return test
369
421
 
@@ -372,45 +424,18 @@ class AuthStorage(Generic[Auth]):
372
424
 
373
425
  return wrapper # type: ignore[return-value]
374
426
 
375
- @classmethod
376
- def add_auth_storage(cls, test: GenericTest) -> AuthStorage:
377
- """Attach a new auth storage instance to the test if it is not already present."""
378
- if not hasattr(test, AUTH_STORAGE_ATTRIBUTE_NAME):
379
- setattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, cls())
380
- else:
381
- raise UsageError(f"`{test.__name__}` has already been decorated with `apply`.")
382
- return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME)
383
-
384
427
  def set(self, case: Case, context: AuthContext) -> None:
385
428
  """Set authentication data on a generated test case."""
386
429
  if not self.is_defined:
387
- raise UsageError("No auth provider is defined.")
430
+ raise IncorrectUsage("No auth provider is defined.")
388
431
  for provider in self.providers:
389
- data: Auth | None = _provider_get(provider, case, context)
432
+ data: Auth | None = provider.get(case, context)
390
433
  if data is not None:
391
434
  provider.set(case, data, context)
435
+ case._has_explicit_auth = True
392
436
  break
393
437
 
394
438
 
395
- def _provider_get(auth_provider: AuthProvider, case: Case, context: AuthContext) -> Auth | None:
396
- # A shim to provide a compatibility layer between previously used convention for `AuthProvider.get`
397
- # where it used to accept a single `context` argument
398
- method = auth_provider.get
399
- parameters = inspect.signature(method).parameters
400
- if len(parameters) == 1:
401
- # Old calling convention
402
- warnings.warn(
403
- "The method 'get' of your AuthProvider is using the old calling convention, "
404
- "which is deprecated and will be removed in Schemathesis 4.0. "
405
- "Please update it to accept both 'case' and 'context' as arguments.",
406
- DeprecationWarning,
407
- stacklevel=1,
408
- )
409
- return method(context) # type: ignore
410
- # New calling convention
411
- return method(case, context)
412
-
413
-
414
439
  def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
415
440
  """Set authentication data on this case.
416
441
 
@@ -424,11 +449,6 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
424
449
  GLOBAL_AUTH_STORAGE.set(case, context)
425
450
 
426
451
 
427
- def get_auth_storage_from_test(test: GenericTest) -> AuthStorage | None:
428
- """Extract the currently attached auth storage from a test function."""
429
- return getattr(test, AUTH_STORAGE_ATTRIBUTE_NAME, None)
430
-
431
-
432
452
  # Global auth API
433
453
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
434
454
  register = GLOBAL_AUTH_STORAGE.register
schemathesis/checks.py CHANGED
@@ -1,68 +1,148 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+
3
3
  import json
4
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
4
5
 
5
- from . import failures
6
- from .exceptions import get_server_error, get_response_parsing_error
7
- from .specs.openapi.checks import (
8
- content_type_conformance,
9
- response_headers_conformance,
10
- response_schema_conformance,
11
- status_code_conformance,
6
+ from schemathesis.core.failures import (
7
+ Failure,
8
+ FailureGroup,
9
+ MalformedJson,
10
+ MaxResponseTimeConfig,
11
+ ResponseTimeExceeded,
12
+ ServerError,
12
13
  )
14
+ from schemathesis.core.registries import Registry
15
+ from schemathesis.core.transport import Response
16
+ from schemathesis.generation.overrides import Override
13
17
 
14
18
  if TYPE_CHECKING:
15
- from .transports.responses import GenericResponse
16
- from .models import Case, CheckFunction
19
+ from requests.models import CaseInsensitiveDict
20
+
21
+ from schemathesis.engine.recorder import ScenarioRecorder
22
+ from schemathesis.generation.case import Case
23
+
24
+ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
25
+ ChecksConfig = dict[CheckFunction, Any]
26
+
27
+
28
+ class CheckContext:
29
+ """Context for Schemathesis checks.
30
+
31
+ Provides access to broader test execution data beyond individual test cases.
32
+ """
33
+
34
+ override: Override | None
35
+ auth: tuple[str, str] | None
36
+ headers: CaseInsensitiveDict | None
37
+ config: ChecksConfig
38
+ transport_kwargs: dict[str, Any] | None
39
+ recorder: ScenarioRecorder | None
40
+
41
+ __slots__ = ("override", "auth", "headers", "config", "transport_kwargs", "recorder")
42
+
43
+ def __init__(
44
+ self,
45
+ override: Override | None,
46
+ auth: tuple[str, str] | None,
47
+ headers: CaseInsensitiveDict | None,
48
+ config: ChecksConfig,
49
+ transport_kwargs: dict[str, Any] | None,
50
+ recorder: ScenarioRecorder | None = None,
51
+ ) -> None:
52
+ self.override = override
53
+ self.auth = auth
54
+ self.headers = headers
55
+ self.config = config
56
+ self.transport_kwargs = transport_kwargs
57
+ self.recorder = recorder
58
+
59
+ def find_parent(self, *, case_id: str) -> Case | None:
60
+ if self.recorder is not None:
61
+ return self.recorder.find_parent(case_id=case_id)
62
+ return None
63
+
64
+ def find_related(self, *, case_id: str) -> Iterator[Case]:
65
+ if self.recorder is not None:
66
+ yield from self.recorder.find_related(case_id=case_id)
17
67
 
68
+ def find_response(self, *, case_id: str) -> Response | None:
69
+ if self.recorder is not None:
70
+ return self.recorder.find_response(case_id=case_id)
71
+ return None
18
72
 
19
- def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
73
+ def record_case(self, *, parent_id: str, case: Case) -> None:
74
+ if self.recorder is not None:
75
+ self.recorder.record_case(parent_id=parent_id, case=case)
76
+
77
+ def record_response(self, *, case_id: str, response: Response) -> None:
78
+ if self.recorder is not None:
79
+ self.recorder.record_response(case_id=case_id, response=response)
80
+
81
+
82
+ CHECKS = Registry[CheckFunction]()
83
+ check = CHECKS.register
84
+
85
+
86
+ @check
87
+ def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
20
88
  """A check to verify that the response is not a server-side error."""
21
- from .specs.graphql.schemas import GraphQLCase
89
+ from .specs.graphql.schemas import GraphQLSchema
22
90
  from .specs.graphql.validation import validate_graphql_response
23
- from .transports.responses import get_json
24
91
 
25
92
  status_code = response.status_code
26
93
  if status_code >= 500:
27
- exc_class = get_server_error(status_code)
28
- raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
29
- if isinstance(case, GraphQLCase):
94
+ raise ServerError(operation=case.operation.label, status_code=status_code)
95
+ if isinstance(case.operation.schema, GraphQLSchema):
30
96
  try:
31
- data = get_json(response)
32
- validate_graphql_response(data)
97
+ data = response.json()
98
+ validate_graphql_response(case, data)
33
99
  except json.JSONDecodeError as exc:
34
- exc_class = get_response_parsing_error(exc)
35
- context = failures.JSONDecodeErrorContext.from_exception(exc)
36
- raise exc_class(context.title, context=context) from exc
100
+ raise MalformedJson.from_exception(operation=case.operation.label, exc=exc) from None
37
101
  return None
38
102
 
39
103
 
40
- DEFAULT_CHECKS: tuple[CheckFunction, ...] = (not_a_server_error,)
41
- OPTIONAL_CHECKS = (
42
- status_code_conformance,
43
- content_type_conformance,
44
- response_headers_conformance,
45
- response_schema_conformance,
46
- )
47
- ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
48
-
49
-
50
- def register(check: CheckFunction) -> CheckFunction:
51
- """Register a new check for schemathesis CLI.
52
-
53
- :param check: A function to validate API responses.
104
+ def max_response_time(ctx: CheckContext, response: Response, case: Case) -> bool | None:
105
+ config = ctx.config.get(max_response_time, MaxResponseTimeConfig())
106
+ elapsed = response.elapsed
107
+ if elapsed > config.limit:
108
+ raise ResponseTimeExceeded(
109
+ operation=case.operation.label,
110
+ message=f"Actual: {elapsed:.2f}ms\nLimit: {config.limit * 1000:.2f}ms",
111
+ elapsed=elapsed,
112
+ deadline=config.limit,
113
+ )
114
+ return None
54
115
 
55
- .. code-block:: python
56
116
 
57
- @schemathesis.check
58
- def new_check(response, case):
59
- # some awesome assertions!
60
- ...
61
- """
62
- from . import cli
117
+ def run_checks(
118
+ *,
119
+ case: Case,
120
+ response: Response,
121
+ ctx: CheckContext,
122
+ checks: Iterable[CheckFunction],
123
+ on_failure: Callable[[str, set[Failure], Failure], None],
124
+ on_success: Callable[[str, Case], None] | None = None,
125
+ ) -> set[Failure]:
126
+ """Run a set of checks against a response."""
127
+ collected: set[Failure] = set()
63
128
 
64
- global ALL_CHECKS
129
+ for check in checks:
130
+ name = check.__name__
131
+ try:
132
+ skip_check = check(ctx, response, case)
133
+ if not skip_check and on_success:
134
+ on_success(name, case)
135
+ except Failure as failure:
136
+ on_failure(name, collected, failure.with_traceback(None))
137
+ except AssertionError as exc:
138
+ custom_failure = Failure.from_assertion(
139
+ name=name,
140
+ operation=case.operation.label,
141
+ exc=exc,
142
+ )
143
+ on_failure(name, collected, custom_failure)
144
+ except FailureGroup as group:
145
+ for sub_failure in group.exceptions:
146
+ on_failure(name, collected, sub_failure)
65
147
 
66
- ALL_CHECKS += (check,)
67
- cli.CHECKS_TYPE.choices += (check.__name__,) # type: ignore
68
- return check
148
+ return collected