schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/__init__.py CHANGED
@@ -1,90 +1,52 @@
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 .hooks import HookContext
10
- from .models import Case
11
- from .specs import openapi
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.transport import Response
8
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
9
+ from schemathesis.generation import GenerationMode, stateful
10
+ from schemathesis.generation.case import Case
11
+ from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
12
+ from schemathesis.hooks import HookContext, hook
13
+ from schemathesis.schemas import APIOperation, BaseSchema
14
+ from schemathesis.transport import SerializationContext, serializer
12
15
 
13
16
  __version__ = SCHEMATHESIS_VERSION
14
17
 
15
- # Default loaders
16
- from_aiohttp = openapi.from_aiohttp
17
- from_asgi = openapi.from_asgi
18
- from_dict = openapi.from_dict
19
- from_file = openapi.from_file
20
- from_path = openapi.from_path
21
- from_pytest_fixture = openapi.from_pytest_fixture
22
- from_uri = openapi.from_uri
23
- from_wsgi = openapi.from_wsgi
24
-
25
- # Public API
26
- auth = auths.GLOBAL_AUTH_STORAGE
27
- check = checks.register
28
- hook = hooks.register
29
- serializer = serializers.register
30
- target = targets.register
31
-
32
- # Backward compatibility
33
- register_check = checks.register
34
- register_target = targets.register
35
- register_string_format = openapi.format
36
-
37
18
  __all__ = [
38
- "auths",
39
- "checks",
40
- "experimental",
41
- "contrib",
42
- "fixups",
43
- "graphql",
44
- "hooks",
45
- "runner",
46
- "serializers",
47
- "targets",
48
- "DataGenerationMethod",
49
- "SCHEMATHESIS_VERSION",
19
+ "__version__",
20
+ # Core data structures
50
21
  "Case",
22
+ "Response",
23
+ "APIOperation",
24
+ "BaseSchema",
25
+ "Config",
26
+ "GenerationMode",
27
+ "stateful",
28
+ # Public errors
29
+ "errors",
30
+ # Spec or usage specific namespaces
51
31
  "openapi",
52
- "__version__",
53
- "from_aiohttp",
54
- "from_asgi",
55
- "from_dict",
56
- "from_file",
57
- "from_path",
58
- "from_pytest_fixture",
59
- "from_uri",
60
- "from_wsgi",
61
- "auth",
62
- "check",
32
+ "graphql",
33
+ "pytest",
34
+ # Hooks
63
35
  "hook",
64
- "serializer",
65
- "target",
66
- "register_check",
67
- "register_target",
68
- "register_string_format",
69
36
  "HookContext",
37
+ # Checks
38
+ "check",
39
+ "CheckContext",
40
+ "CheckFunction",
41
+ # Auth
42
+ "auth",
43
+ "AuthContext",
44
+ "AuthProvider",
45
+ # Targeted Property-based Testing
46
+ "metric",
47
+ "MetricContext",
48
+ "MetricFunction",
49
+ # Serialization
50
+ "serializer",
51
+ "SerializationContext",
70
52
  ]
71
-
72
-
73
- def _load_generic_response() -> Any:
74
- from .transports.responses import GenericResponse
75
-
76
- return GenericResponse
77
-
78
-
79
- def _load_base_schema() -> Any:
80
- from .schemas import BaseSchema
81
-
82
- return BaseSchema
83
-
84
-
85
- _imports = {"GenericResponse": _load_generic_response, "BaseSchema": _load_base_schema}
86
-
87
-
88
- def __getattr__(name: str) -> Any:
89
- # Some modules are relatively heavy, hence load them lazily to improve startup time for CLI
90
- 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,30 +17,51 @@ 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
 
36
35
  @dataclass
37
36
  class AuthContext:
38
- """Holds state relevant for the authentication process.
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
+ ```
39
56
 
40
- :ivar APIOperation operation: API operation that is currently being processed.
41
- :ivar app: Optional Python application if the WSGI / ASGI integration is used.
42
57
  """
43
58
 
44
59
  operation: APIOperation
60
+ """API operation currently being processed for authentication."""
45
61
  app: Any | None
62
+ """Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
63
+
64
+ __slots__ = ("operation", "app")
46
65
 
47
66
 
48
67
  CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
@@ -50,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
50
69
 
51
70
  @runtime_checkable
52
71
  class AuthProvider(Generic[Auth], Protocol):
53
- """Get authentication data for an API and set it on the generated test cases."""
72
+ """Protocol for implementing custom authentication in API tests."""
54
73
 
55
- def get(self, case: Case, context: AuthContext) -> Auth | None:
56
- """Get the authentication data.
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`.
57
83
 
58
- :param Case case: Generated test case.
59
- :param AuthContext context: Holds state relevant for the authentication process.
60
- :return: Any authentication data you find useful for your use case. For example, it could be an access token.
61
84
  """
62
85
 
63
- def set(self, case: Case, data: Auth, context: AuthContext) -> None:
64
- """Set authentication data on a generated test case.
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.
65
93
 
66
- :param Optional[Auth] data: Authentication data you got from the ``get`` method.
67
- :param Case case: Generated test case.
68
- :param AuthContext context: Holds state relevant for the authentication process.
69
94
  """
70
95
 
71
96
 
@@ -111,7 +136,7 @@ class CachingAuthProvider(Generic[Auth]):
111
136
  # Another thread updated the cache
112
137
  return cache_entry.data
113
138
  # 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]
139
+ data: Auth = self.provider.get(case, context) # type: ignore[assignment]
115
140
  self._set_cache_entry(data, case, context)
116
141
  return data
117
142
  return cache_entry.data
@@ -152,8 +177,7 @@ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
152
177
  class FilterableRegisterAuth(Protocol):
153
178
  """Protocol that adds filters to the return value of `register`."""
154
179
 
155
- def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]:
156
- pass
180
+ def __call__(self, provider_class: type[AuthProvider]) -> type[AuthProvider]: ...
157
181
 
158
182
  def apply_to(
159
183
  self,
@@ -165,8 +189,7 @@ class FilterableRegisterAuth(Protocol):
165
189
  method_regex: str | None = None,
166
190
  path: FilterValue | None = None,
167
191
  path_regex: str | None = None,
168
- ) -> FilterableRegisterAuth:
169
- pass
192
+ ) -> FilterableRegisterAuth: ...
170
193
 
171
194
  def skip_for(
172
195
  self,
@@ -178,15 +201,13 @@ class FilterableRegisterAuth(Protocol):
178
201
  method_regex: str | None = None,
179
202
  path: FilterValue | None = None,
180
203
  path_regex: str | None = None,
181
- ) -> FilterableRegisterAuth:
182
- pass
204
+ ) -> FilterableRegisterAuth: ...
183
205
 
184
206
 
185
207
  class FilterableApplyAuth(Protocol):
186
208
  """Protocol that adds filters to the return value of `apply`."""
187
209
 
188
- def __call__(self, test: GenericTest) -> GenericTest:
189
- pass
210
+ def __call__(self, test: Callable) -> Callable: ...
190
211
 
191
212
  def apply_to(
192
213
  self,
@@ -198,8 +219,7 @@ class FilterableApplyAuth(Protocol):
198
219
  method_regex: str | None = None,
199
220
  path: FilterValue | None = None,
200
221
  path_regex: str | None = None,
201
- ) -> FilterableApplyAuth:
202
- pass
222
+ ) -> FilterableApplyAuth: ...
203
223
 
204
224
  def skip_for(
205
225
  self,
@@ -211,8 +231,7 @@ class FilterableApplyAuth(Protocol):
211
231
  method_regex: str | None = None,
212
232
  path: FilterValue | None = None,
213
233
  path_regex: str | None = None,
214
- ) -> FilterableApplyAuth:
215
- pass
234
+ ) -> FilterableApplyAuth: ...
216
235
 
217
236
 
218
237
  class FilterableRequestsAuth(Protocol):
@@ -228,8 +247,7 @@ class FilterableRequestsAuth(Protocol):
228
247
  method_regex: str | None = None,
229
248
  path: FilterValue | None = None,
230
249
  path_regex: str | None = None,
231
- ) -> FilterableRequestsAuth:
232
- pass
250
+ ) -> FilterableRequestsAuth: ...
233
251
 
234
252
  def skip_for(
235
253
  self,
@@ -241,8 +259,7 @@ class FilterableRequestsAuth(Protocol):
241
259
  method_regex: str | None = None,
242
260
  path: FilterValue | None = None,
243
261
  path_regex: str | None = None,
244
- ) -> FilterableRequestsAuth:
245
- pass
262
+ ) -> FilterableRequestsAuth: ...
246
263
 
247
264
 
248
265
  @dataclass
@@ -254,7 +271,7 @@ class SelectiveAuthProvider(Generic[Auth]):
254
271
 
255
272
  def get(self, case: Case, context: AuthContext) -> Auth | None:
256
273
  if self.filter_set.match(context):
257
- return _provider_get(self.provider, case, context)
274
+ return self.provider.get(case, context)
258
275
  return None
259
276
 
260
277
  def set(self, case: Case, data: Auth, context: AuthContext) -> None:
@@ -278,8 +295,7 @@ class AuthStorage(Generic[Auth]):
278
295
  *,
279
296
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
280
297
  cache_by_key: CacheKeyFunction | None = None,
281
- ) -> FilterableRegisterAuth:
282
- pass
298
+ ) -> FilterableRegisterAuth: ...
283
299
 
284
300
  @overload
285
301
  def __call__(
@@ -288,8 +304,7 @@ class AuthStorage(Generic[Auth]):
288
304
  *,
289
305
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
290
306
  cache_by_key: CacheKeyFunction | None = None,
291
- ) -> FilterableApplyAuth:
292
- pass
307
+ ) -> FilterableApplyAuth: ...
293
308
 
294
309
  def __call__(
295
310
  self,
@@ -300,15 +315,14 @@ class AuthStorage(Generic[Auth]):
300
315
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
301
316
  if provider_class is not None:
302
317
  return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
303
- return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
318
+ return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
304
319
 
305
320
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
306
321
  """Use `requests` auth instance as an auth provider."""
307
322
  filter_set = FilterSet()
308
323
  self.providers.append(SelectiveAuthProvider(provider=RequestsAuth(auth), filter_set=filter_set))
309
324
 
310
- class _FilterableRequestsAuth:
311
- pass
325
+ class _FilterableRequestsAuth: ...
312
326
 
313
327
  attach_filter_chain(_FilterableRequestsAuth, "apply_to", filter_set.include)
314
328
  attach_filter_chain(_FilterableRequestsAuth, "skip_for", filter_set.exclude)
@@ -325,8 +339,9 @@ class AuthStorage(Generic[Auth]):
325
339
  ) -> None:
326
340
  if not issubclass(provider_class, AuthProvider):
327
341
  raise TypeError(
328
- f"`{provider_class.__name__}` is not a valid auth provider. "
329
- f"Check `schemathesis.auths.AuthProvider` documentation for examples."
342
+ f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
343
+ f"Auth providers must have `get` and `set` methods. "
344
+ f"See `schemathesis.AuthProvider` documentation for examples."
330
345
  )
331
346
  provider: AuthProvider
332
347
  # Apply caching if desired
@@ -345,30 +360,12 @@ class AuthStorage(Generic[Auth]):
345
360
  provider = SelectiveAuthProvider(provider, filter_set)
346
361
  self.providers.append(provider)
347
362
 
348
- def register(
363
+ def auth(
349
364
  self,
350
365
  *,
351
366
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
352
367
  cache_by_key: CacheKeyFunction | None = None,
353
368
  ) -> FilterableRegisterAuth:
354
- """Register a new auth provider.
355
-
356
- .. code-block:: python
357
-
358
- @schemathesis.auth()
359
- class TokenAuth:
360
- def get(self, context):
361
- response = requests.post(
362
- "https://example.schemathesis.io/api/token/",
363
- json={"username": "demo", "password": "test"},
364
- )
365
- data = response.json()
366
- return data["access_token"]
367
-
368
- def set(self, case, data, context):
369
- # Modify `case` the way you need
370
- case.headers = {"Authorization": f"Bearer {data}"}
371
- """
372
369
  filter_set = FilterSet()
373
370
 
374
371
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
@@ -399,27 +396,13 @@ class AuthStorage(Generic[Auth]):
399
396
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
400
397
  cache_by_key: CacheKeyFunction | None = None,
401
398
  ) -> FilterableApplyAuth:
402
- """Register auth provider only on one test function.
403
-
404
- :param Type[AuthProvider] provider_class: Authentication provider class.
405
- :param Optional[int] refresh_interval: Cache duration in seconds.
406
-
407
- .. code-block:: python
408
-
409
- class Auth:
410
- ...
411
-
412
-
413
- @schema.auth(Auth)
414
- @schema.parametrize()
415
- def test_api(case):
416
- ...
417
-
418
- """
419
399
  filter_set = FilterSet()
420
400
 
421
- def wrapper(test: GenericTest) -> GenericTest:
422
- auth_storage = self.add_auth_storage(test)
401
+ def wrapper(test: Callable) -> Callable:
402
+ if AuthStorageMark.is_set(test):
403
+ raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `apply`.")
404
+ auth_storage = self.__class__()
405
+ AuthStorageMark.set(test, auth_storage)
423
406
  auth_storage._set_provider(
424
407
  provider_class=provider_class,
425
408
  refresh_interval=refresh_interval,
@@ -433,46 +416,18 @@ class AuthStorage(Generic[Auth]):
433
416
 
434
417
  return wrapper # type: ignore[return-value]
435
418
 
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
419
  def set(self, case: Case, context: AuthContext) -> None:
446
420
  """Set authentication data on a generated test case."""
447
421
  if not self.is_defined:
448
- raise UsageError("No auth provider is defined.")
422
+ raise IncorrectUsage("No auth provider is defined.")
449
423
  for provider in self.providers:
450
- data: Auth | None = _provider_get(provider, case, context)
424
+ data: Auth | None = provider.get(case, context)
451
425
  if data is not None:
452
426
  provider.set(case, data, context)
453
427
  case._has_explicit_auth = True
454
428
  break
455
429
 
456
430
 
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
431
  def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | None) -> None:
477
432
  """Set authentication data on this case.
478
433
 
@@ -486,12 +441,46 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
486
441
  GLOBAL_AUTH_STORAGE.set(case, context)
487
442
 
488
443
 
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
444
  # Global auth API
495
445
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
496
- register = GLOBAL_AUTH_STORAGE.register
497
446
  unregister = GLOBAL_AUTH_STORAGE.unregister
447
+
448
+
449
+ def auth(
450
+ *,
451
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
452
+ cache_by_key: CacheKeyFunction | None = None,
453
+ ) -> FilterableRegisterAuth:
454
+ """Register a dynamic authentication provider for APIs with expiring tokens.
455
+
456
+ Args:
457
+ refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
458
+ cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
459
+
460
+ Example:
461
+ ```python
462
+ import schemathesis
463
+ import requests
464
+
465
+ @schemathesis.auth()
466
+ class TokenAuth:
467
+ def get(self, case, context):
468
+ \"\"\"Fetch fresh authentication token\"\"\"
469
+ response = requests.post(
470
+ "http://localhost:8000/auth/token",
471
+ json={"username": "demo", "password": "test"}
472
+ )
473
+ return response.json()["access_token"]
474
+
475
+ def set(self, case, data, context):
476
+ \"\"\"Apply token to test case headers\"\"\"
477
+ case.headers = case.headers or {}
478
+ case.headers["Authorization"] = f"Bearer {data}"
479
+ ```
480
+
481
+ """
482
+ return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
483
+
484
+
485
+ auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
486
+ auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]