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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/__init__.py CHANGED
@@ -1,28 +1,56 @@
1
- from ._compat import _install_hypothesis_jsonschema_compatibility_shim
1
+ from __future__ import annotations
2
2
 
3
- _install_hypothesis_jsonschema_compatibility_shim()
3
+ from schemathesis import errors, graphql, openapi, pytest
4
+ from schemathesis.auths import AuthContext, AuthProvider, auth
5
+ from schemathesis.checks import CheckContext, CheckFunction, check
6
+ from schemathesis.config import SchemathesisConfig as Config
7
+ from schemathesis.core.deserialization import DeserializationContext, deserializer
8
+ from schemathesis.core.transport import Response
9
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
10
+ from schemathesis.generation import GenerationMode, stateful
11
+ from schemathesis.generation.case import Case
12
+ from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
13
+ from schemathesis.hooks import HookContext, hook
14
+ from schemathesis.schemas import APIOperation, BaseSchema
15
+ from schemathesis.transport import SerializationContext, serializer
4
16
 
5
- del _install_hypothesis_jsonschema_compatibility_shim
17
+ __version__ = SCHEMATHESIS_VERSION
6
18
 
7
- from . import fixups, hooks, runner, serializers, targets
8
- from .cli import register_check, register_target
9
- from .constants import DataGenerationMethod, __version__
10
- from .models import Case
11
- from .specs import graphql, openapi
12
- from .specs.openapi._hypothesis import init_default_strategies, register_string_format
13
- from .utils import GenericResponse
14
-
15
- init_default_strategies()
16
-
17
- # Is not a part of the public API
18
- del init_default_strategies
19
-
20
- # Default loaders
21
- from_aiohttp = openapi.from_aiohttp
22
- from_asgi = openapi.from_asgi
23
- from_dict = openapi.from_dict
24
- from_file = openapi.from_file
25
- from_path = openapi.from_path
26
- from_pytest_fixture = openapi.from_pytest_fixture
27
- from_uri = openapi.from_uri
28
- from_wsgi = openapi.from_wsgi
19
+ __all__ = [
20
+ "__version__",
21
+ # Core data structures
22
+ "Case",
23
+ "Response",
24
+ "APIOperation",
25
+ "BaseSchema",
26
+ "Config",
27
+ "GenerationMode",
28
+ "stateful",
29
+ # Public errors
30
+ "errors",
31
+ # Spec or usage specific namespaces
32
+ "openapi",
33
+ "graphql",
34
+ "pytest",
35
+ # Hooks
36
+ "hook",
37
+ "HookContext",
38
+ # Checks
39
+ "check",
40
+ "CheckContext",
41
+ "CheckFunction",
42
+ # Auth
43
+ "auth",
44
+ "AuthContext",
45
+ "AuthProvider",
46
+ # Targeted Property-based Testing
47
+ "metric",
48
+ "MetricContext",
49
+ "MetricFunction",
50
+ # Response deserialization
51
+ "deserializer",
52
+ "DeserializationContext",
53
+ # Serialization
54
+ "serializer",
55
+ "SerializationContext",
56
+ ]
schemathesis/auths.py ADDED
@@ -0,0 +1,507 @@
1
+ """Support for custom API authentication mechanisms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Callable,
12
+ Generic,
13
+ Protocol,
14
+ TypeVar,
15
+ Union,
16
+ overload,
17
+ runtime_checkable,
18
+ )
19
+
20
+ from schemathesis.core.errors import AuthenticationError, 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
+
25
+ if TYPE_CHECKING:
26
+ import requests.auth
27
+
28
+ from schemathesis.schemas import APIOperation
29
+
30
+ DEFAULT_REFRESH_INTERVAL = 300
31
+ AuthStorageMark = Mark["AuthStorage"](attr_name="auth_storage")
32
+ Auth = TypeVar("Auth")
33
+
34
+
35
+ @dataclass
36
+ class AuthContext:
37
+ """Runtime context passed to authentication providers during token generation.
38
+
39
+ Provides access to the current API operation and application instance when
40
+ auth providers need operation-specific tokens or application state.
41
+
42
+ Example:
43
+ ```python
44
+ @schemathesis.auth()
45
+ class ContextAwareAuth:
46
+ def get(self, case, context):
47
+ # Access operation details
48
+ if "/admin/" in context.operation.path:
49
+ return self.get_admin_token()
50
+ else:
51
+ return self.get_user_token()
52
+
53
+ def set(self, case, data, context):
54
+ case.headers = {"Authorization": f"Bearer {data}"}
55
+ ```
56
+
57
+ """
58
+
59
+ operation: APIOperation
60
+ """API operation currently being processed for authentication."""
61
+ app: Any | None
62
+ """Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
63
+
64
+ __slots__ = ("operation", "app")
65
+
66
+
67
+ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
68
+
69
+
70
+ @runtime_checkable
71
+ class AuthProvider(Generic[Auth], Protocol):
72
+ """Protocol for implementing custom authentication in API tests."""
73
+
74
+ def get(self, case: Case, ctx: AuthContext) -> Auth | None:
75
+ """Obtain authentication data for the test case.
76
+
77
+ Args:
78
+ case: Generated test case requiring authentication.
79
+ ctx: Authentication state and configuration.
80
+
81
+ Returns:
82
+ Authentication data (e.g., token, credentials) or `None`.
83
+
84
+ """
85
+
86
+ def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
87
+ """Apply authentication data to the test case.
88
+
89
+ Args:
90
+ case: Test case to modify.
91
+ data: Authentication data from the `get` method.
92
+ ctx: Authentication state and configuration.
93
+
94
+ """
95
+
96
+
97
+ @dataclass
98
+ class CacheEntry(Generic[Auth]):
99
+ """Cached auth data."""
100
+
101
+ data: Auth
102
+ expires: float
103
+
104
+
105
+ @dataclass
106
+ class RequestsAuth(Generic[Auth]):
107
+ """Provider that sets auth data via `requests` auth instance."""
108
+
109
+ auth: requests.auth.AuthBase
110
+
111
+ def get(self, _: Case, __: AuthContext) -> Auth | None:
112
+ return self.auth # type: ignore[return-value]
113
+
114
+ def set(self, case: Case, _: Auth, __: AuthContext) -> None:
115
+ case._auth = self.auth
116
+
117
+
118
+ @dataclass
119
+ class CachingAuthProvider(Generic[Auth]):
120
+ """Caches the underlying auth provider."""
121
+
122
+ provider: AuthProvider
123
+ refresh_interval: int = DEFAULT_REFRESH_INTERVAL
124
+ cache_entry: CacheEntry[Auth] | None = None
125
+ # The timer exists here to simplify testing
126
+ timer: Callable[[], float] = time.monotonic
127
+ _refresh_lock: threading.Lock = field(default_factory=threading.Lock)
128
+
129
+ def get(self, case: Case, context: AuthContext) -> Auth | None:
130
+ """Get cached auth value."""
131
+ __tracebackhide__ = True
132
+ cache_entry = self._get_cache_entry(case, context)
133
+ if cache_entry is None or self.timer() >= cache_entry.expires:
134
+ with self._refresh_lock:
135
+ cache_entry = self._get_cache_entry(case, context)
136
+ if not (cache_entry is None or self.timer() >= cache_entry.expires):
137
+ # Another thread updated the cache
138
+ return cache_entry.data
139
+ # We know that optional auth is possible only inside a higher-level wrapper
140
+ try:
141
+ data: Auth = self.provider.get(case, context) # type: ignore[assignment]
142
+ except Exception as exc:
143
+ provider_name = self.provider.__class__.__name__
144
+ raise AuthenticationError(provider_name, "get", str(exc)) from exc
145
+ self._set_cache_entry(data, case, context)
146
+ return data
147
+ return cache_entry.data
148
+
149
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
150
+ return self.cache_entry
151
+
152
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
153
+ self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
154
+
155
+ def set(self, case: Case, data: Auth, context: AuthContext) -> None:
156
+ """Set auth data on the `Case` instance.
157
+
158
+ This implementation delegates this to the actual provider.
159
+ """
160
+ self.provider.set(case, data, context)
161
+
162
+
163
+ def _noop_key_function(case: Case, context: AuthContext) -> str:
164
+ # Never used
165
+ raise NotImplementedError
166
+
167
+
168
+ @dataclass
169
+ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
170
+ cache_by_key: CacheKeyFunction = _noop_key_function
171
+ cache_entries: dict[str | int, CacheEntry[Auth] | None] = field(default_factory=dict)
172
+
173
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
174
+ key = self.cache_by_key(case, context)
175
+ return self.cache_entries.get(key)
176
+
177
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
178
+ key = self.cache_by_key(case, context)
179
+ self.cache_entries[key] = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
180
+
181
+
182
+ class FilterableRegisterAuth(Protocol):
183
+ """Protocol that adds filters to the return value of `register`."""
184
+
185
+ def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
186
+
187
+ def apply_to(
188
+ self,
189
+ func: MatcherFunc | None = None,
190
+ *,
191
+ name: FilterValue | None = None,
192
+ name_regex: str | None = None,
193
+ method: FilterValue | None = None,
194
+ method_regex: str | None = None,
195
+ path: FilterValue | None = None,
196
+ path_regex: str | None = None,
197
+ ) -> FilterableRegisterAuth: ...
198
+
199
+ def skip_for(
200
+ self,
201
+ func: MatcherFunc | None = None,
202
+ *,
203
+ name: FilterValue | None = None,
204
+ name_regex: str | None = None,
205
+ method: FilterValue | None = None,
206
+ method_regex: str | None = None,
207
+ path: FilterValue | None = None,
208
+ path_regex: str | None = None,
209
+ ) -> FilterableRegisterAuth: ...
210
+
211
+
212
+ class FilterableApplyAuth(Protocol):
213
+ """Protocol that adds filters to the return value of `apply`."""
214
+
215
+ def __call__(self, test: Callable) -> Callable: ...
216
+
217
+ def apply_to(
218
+ self,
219
+ func: MatcherFunc | None = None,
220
+ *,
221
+ name: FilterValue | None = None,
222
+ name_regex: str | None = None,
223
+ method: FilterValue | None = None,
224
+ method_regex: str | None = None,
225
+ path: FilterValue | None = None,
226
+ path_regex: str | None = None,
227
+ ) -> FilterableApplyAuth: ...
228
+
229
+ def skip_for(
230
+ self,
231
+ func: MatcherFunc | None = None,
232
+ *,
233
+ name: FilterValue | None = None,
234
+ name_regex: str | None = None,
235
+ method: FilterValue | None = None,
236
+ method_regex: str | None = None,
237
+ path: FilterValue | None = None,
238
+ path_regex: str | None = None,
239
+ ) -> FilterableApplyAuth: ...
240
+
241
+
242
+ class FilterableRequestsAuth(Protocol):
243
+ """Protocol that adds filters to the return value of `set_from_requests`."""
244
+
245
+ def apply_to(
246
+ self,
247
+ func: MatcherFunc | None = None,
248
+ *,
249
+ name: FilterValue | None = None,
250
+ name_regex: str | None = None,
251
+ method: FilterValue | None = None,
252
+ method_regex: str | None = None,
253
+ path: FilterValue | None = None,
254
+ path_regex: str | None = None,
255
+ ) -> FilterableRequestsAuth: ...
256
+
257
+ def skip_for(
258
+ self,
259
+ func: MatcherFunc | None = None,
260
+ *,
261
+ name: FilterValue | None = None,
262
+ name_regex: str | None = None,
263
+ method: FilterValue | None = None,
264
+ method_regex: str | None = None,
265
+ path: FilterValue | None = None,
266
+ path_regex: str | None = None,
267
+ ) -> FilterableRequestsAuth: ...
268
+
269
+
270
+ @dataclass
271
+ class SelectiveAuthProvider(Generic[Auth]):
272
+ """Applies auth depending on the configured filters."""
273
+
274
+ provider: AuthProvider
275
+ filter_set: FilterSet
276
+
277
+ def get(self, case: Case, context: AuthContext) -> Auth | None:
278
+ __tracebackhide__ = True
279
+ if self.filter_set.match(context):
280
+ try:
281
+ return self.provider.get(case, context)
282
+ except AuthenticationError:
283
+ # Already wrapped, re-raise as-is
284
+ raise
285
+ except Exception as exc:
286
+ # Need to unwrap to get the actual provider class name
287
+ provider = self.provider
288
+ # Unwrap caching providers
289
+ while isinstance(provider, (CachingAuthProvider, KeyedCachingAuthProvider)):
290
+ provider = provider.provider
291
+ provider_name = provider.__class__.__name__
292
+ raise AuthenticationError(provider_name, "get", str(exc)) from exc
293
+ return None
294
+
295
+ def set(self, case: Case, data: Auth, context: AuthContext) -> None:
296
+ __tracebackhide__ = True
297
+ self.provider.set(case, data, context)
298
+
299
+
300
+ @dataclass
301
+ class AuthStorage(Generic[Auth]):
302
+ """Store and manage API authentication."""
303
+
304
+ providers: list[AuthProvider] = field(default_factory=list)
305
+
306
+ @property
307
+ def is_defined(self) -> bool:
308
+ """Whether there is an auth provider set."""
309
+ return bool(self.providers)
310
+
311
+ @overload
312
+ def __call__(
313
+ self,
314
+ *,
315
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
316
+ cache_by_key: CacheKeyFunction | None = None,
317
+ ) -> FilterableRegisterAuth: ...
318
+
319
+ @overload
320
+ def __call__(
321
+ self,
322
+ provider_class: type[AuthProvider],
323
+ *,
324
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
325
+ cache_by_key: CacheKeyFunction | None = None,
326
+ ) -> FilterableApplyAuth: ...
327
+
328
+ def __call__(
329
+ self,
330
+ provider_class: type[AuthProvider] | None = None,
331
+ *,
332
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
333
+ cache_by_key: CacheKeyFunction | None = None,
334
+ ) -> FilterableRegisterAuth | FilterableApplyAuth:
335
+ if provider_class is not None:
336
+ return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
337
+ return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
338
+
339
+ def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
340
+ """Use `requests` auth instance as an auth provider."""
341
+ filter_set = FilterSet()
342
+ self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
343
+
344
+ class _FilterableRequestsAuth: ...
345
+
346
+ attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
347
+ attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
348
+
349
+ return _FilterableRequestsAuth # type: ignore[return-value]
350
+
351
+ def _set_provider(
352
+ self,
353
+ *,
354
+ provider_class: type[AuthProvider],
355
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
356
+ cache_by_key: CacheKeyFunction | None = None,
357
+ filter_set: FilterSet,
358
+ ) -> None:
359
+ if not issubclass(provider_class, AuthProvider):
360
+ raise TypeError(
361
+ f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
362
+ f"Auth providers must have `get` and `set` methods. "
363
+ f"See `schemathesis.AuthProvider` documentation for examples."
364
+ )
365
+ provider: AuthProvider
366
+ # Apply caching if desired
367
+ instance = provider_class()
368
+ if refresh_interval is not None:
369
+ if cache_by_key is None:
370
+ provider = CachingAuthProvider(instance, refresh_interval=refresh_interval)
371
+ else:
372
+ provider = KeyedCachingAuthProvider(
373
+ instance, refresh_interval=refresh_interval, cache_by_key=cache_by_key
374
+ )
375
+ else:
376
+ provider = instance
377
+ # Store filters if any
378
+ if not filter_set.is_empty():
379
+ provider = SelectiveAuthProvider(provider, filter_set)
380
+ self.providers.append(provider)
381
+
382
+ def auth(
383
+ self,
384
+ *,
385
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
386
+ cache_by_key: CacheKeyFunction | None = None,
387
+ ) -> FilterableRegisterAuth:
388
+ filter_set = FilterSet()
389
+
390
+ def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
391
+ self._set_provider(
392
+ provider_class=provider_class,
393
+ refresh_interval=refresh_interval,
394
+ filter_set=filter_set,
395
+ cache_by_key=cache_by_key,
396
+ )
397
+ return provider_class
398
+
399
+ attach_filter_chain(wrapper, "apply_to", filter_set.include)
400
+ attach_filter_chain(wrapper, "skip_for", filter_set.exclude)
401
+
402
+ return wrapper # type: ignore[return-value]
403
+
404
+ def unregister(self) -> None:
405
+ """Unregister the currently registered auth provider.
406
+
407
+ No-op if there is no auth provider registered.
408
+ """
409
+ self.providers = []
410
+
411
+ def apply(
412
+ self,
413
+ provider_class: type[AuthProvider],
414
+ *,
415
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
416
+ cache_by_key: CacheKeyFunction | None = None,
417
+ ) -> FilterableApplyAuth:
418
+ filter_set = FilterSet()
419
+
420
+ def wrapper(test: Callable) -> Callable:
421
+ if AuthStorageMark.is_set(test):
422
+ raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `apply`.")
423
+ auth_storage = self.__class__()
424
+ AuthStorageMark.set(test, auth_storage)
425
+ auth_storage._set_provider(
426
+ provider_class=provider_class,
427
+ refresh_interval=refresh_interval,
428
+ filter_set=filter_set,
429
+ cache_by_key=cache_by_key,
430
+ )
431
+ return test
432
+
433
+ attach_filter_chain(wrapper, "apply_to", filter_set.include)
434
+ attach_filter_chain(wrapper, "skip_for", filter_set.exclude)
435
+
436
+ return wrapper # type: ignore[return-value]
437
+
438
+ def set(self, case: Case, context: AuthContext) -> None:
439
+ """Set authentication data on a generated test case."""
440
+ __tracebackhide__ = True
441
+ if not self.is_defined:
442
+ raise IncorrectUsage("No auth provider is defined.")
443
+ for provider in self.providers:
444
+ data: Auth | None = provider.get(case, context)
445
+ if data is not None:
446
+ provider.set(case, data, context)
447
+ case._has_explicit_auth = True
448
+ break
449
+
450
+
451
+ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
452
+ """Set authentication data on this case.
453
+
454
+ If there is no auth defined, then this function is no-op.
455
+ """
456
+ __tracebackhide__ = True
457
+ if auth_storage is not None:
458
+ auth_storage.set(case, context)
459
+ elif case.operation.schema.auth.is_defined:
460
+ case.operation.schema.auth.set(case, context)
461
+ elif GLOBAL_AUTH_STORAGE.is_defined:
462
+ GLOBAL_AUTH_STORAGE.set(case, context)
463
+
464
+
465
+ # Global auth API
466
+ GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
467
+ unregister = GLOBAL_AUTH_STORAGE.unregister
468
+
469
+
470
+ def auth(
471
+ *,
472
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
473
+ cache_by_key: CacheKeyFunction | None = None,
474
+ ) -> FilterableRegisterAuth:
475
+ """Register a dynamic authentication provider for APIs with expiring tokens.
476
+
477
+ Args:
478
+ refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
479
+ cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
480
+
481
+ Example:
482
+ ```python
483
+ import schemathesis
484
+ import requests
485
+
486
+ @schemathesis.auth()
487
+ class TokenAuth:
488
+ def get(self, case, context):
489
+ \"\"\"Fetch fresh authentication token\"\"\"
490
+ response = requests.post(
491
+ "http://localhost:8000/auth/token",
492
+ json={"username": "demo", "password": "test"}
493
+ )
494
+ return response.json()["access_token"]
495
+
496
+ def set(self, case, data, context):
497
+ \"\"\"Apply token to test case headers\"\"\"
498
+ case.headers = case.headers or {}
499
+ case.headers["Authorization"] = f"Bearer {data}"
500
+ ```
501
+
502
+ """
503
+ return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
504
+
505
+
506
+ auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
507
+ auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]