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/schemas.py CHANGED
@@ -1,122 +1,262 @@
1
- """Schema objects provide a convenient interface to raw schemas.
1
+ from __future__ import annotations
2
2
 
3
- Their responsibilities:
4
- - Provide a unified way to work with different types of schemas
5
- - Give all paths / methods combinations that are available directly from the schema;
6
-
7
- They give only static definitions of paths.
8
- """
9
3
  from collections.abc import Mapping
10
- from difflib import get_close_matches
11
- from functools import lru_cache
4
+ from dataclasses import dataclass, field
5
+ from functools import cached_property, lru_cache, partial
6
+ from itertools import chain
12
7
  from typing import (
8
+ TYPE_CHECKING,
13
9
  Any,
14
10
  Callable,
15
- Dict,
16
11
  Generator,
17
- Iterable,
12
+ Generic,
18
13
  Iterator,
19
- List,
20
14
  NoReturn,
21
- Optional,
22
- Sequence,
23
- Tuple,
24
- Type,
25
15
  TypeVar,
26
- Union,
27
16
  )
28
17
  from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
29
18
 
30
- import attr
31
- import hypothesis
32
- from hypothesis.strategies import SearchStrategy
33
- from requests.structures import CaseInsensitiveDict
34
-
35
- from ._hypothesis import create_test
36
- from .constants import DEFAULT_DATA_GENERATION_METHODS, CodeSampleStyle, DataGenerationMethod
37
- from .exceptions import InvalidSchema, UsageError
38
- from .hooks import HookContext, HookDispatcher, HookScope, dispatch
39
- from .models import APIOperation, Case
40
- from .stateful import APIStateMachine, Stateful, StatefulTest
41
- from .types import (
42
- Body,
43
- Cookies,
44
- DataGenerationMethodInput,
45
- Filter,
46
- FormData,
47
- GenericTest,
48
- Headers,
49
- NotSet,
50
- PathParameters,
51
- Query,
19
+ from schemathesis import transport
20
+ from schemathesis.config import ProjectConfig
21
+ from schemathesis.core import NOT_SET, NotSet, media_types
22
+ from schemathesis.core.adapter import OperationParameter, ResponsesContainer
23
+ from schemathesis.core.errors import IncorrectUsage, InvalidSchema
24
+ from schemathesis.core.result import Ok, Result
25
+ from schemathesis.core.transport import Response
26
+ from schemathesis.generation import GenerationMode
27
+ from schemathesis.generation.case import Case
28
+ from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
29
+ from schemathesis.generation.meta import CaseMetadata
30
+ from schemathesis.hooks import HookDispatcherMark, _should_skip_hook
31
+
32
+ from .auths import AuthStorage
33
+ from .filters import (
34
+ FilterSet,
35
+ FilterValue,
36
+ MatcherFunc,
37
+ RegexValue,
38
+ is_deprecated,
52
39
  )
53
- from .utils import NOT_SET, PARAMETRIZE_MARKER, GenericResponse, GivenInput, Ok, Result, given_proxy
40
+ from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
54
41
 
42
+ if TYPE_CHECKING:
43
+ import httpx
44
+ import requests
45
+ from hypothesis.strategies import SearchStrategy
46
+ from requests.structures import CaseInsensitiveDict
47
+ from werkzeug.test import TestResponse
55
48
 
56
- class MethodsDict(CaseInsensitiveDict):
57
- """Container for accessing API operations.
49
+ from schemathesis.core import Specification
50
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
58
51
 
59
- Provides a more specific error message if API operation is not found.
60
- """
61
52
 
62
- def __getitem__(self, item: Any) -> Any:
63
- try:
64
- return super().__getitem__(item)
65
- except KeyError as exc:
66
- available_methods = ", ".join(map(str.upper, self))
67
- message = f"Method `{item}` not found. Available methods: {available_methods}"
68
- raise KeyError(message) from exc
53
+ @lru_cache
54
+ def get_full_path(base_path: str, path: str) -> str:
55
+ return unquote(urljoin(base_path, quote(path.lstrip("/"))))
69
56
 
70
57
 
71
- C = TypeVar("C", bound=Case)
58
+ @dataclass
59
+ class FilteredCount:
60
+ """Count of total items and those passing filters."""
72
61
 
62
+ total: int
63
+ selected: int
73
64
 
74
- @lru_cache()
75
- def get_full_path(base_path: str, path: str) -> str:
76
- return unquote(urljoin(base_path, quote(path.lstrip("/")))) # pragma: no mutate
65
+ __slots__ = ("total", "selected")
66
+
67
+ def __init__(self) -> None:
68
+ self.total = 0
69
+ self.selected = 0
70
+
71
+
72
+ @dataclass
73
+ class ApiStatistic:
74
+ """Statistics about API operations and links."""
75
+
76
+ operations: FilteredCount
77
+ links: FilteredCount
78
+
79
+ __slots__ = ("operations", "links")
77
80
 
81
+ def __init__(self) -> None:
82
+ self.operations = FilteredCount()
83
+ self.links = FilteredCount()
78
84
 
79
- @attr.s(eq=False) # pragma: no mutate
85
+
86
+ @dataclass
87
+ class ApiOperationsCount:
88
+ """Statistics about API operations."""
89
+
90
+ total: int
91
+ selected: int
92
+
93
+ __slots__ = ("total", "selected")
94
+
95
+ def __init__(self) -> None:
96
+ self.total = 0
97
+ self.selected = 0
98
+
99
+
100
+ @dataclass(eq=False)
80
101
  class BaseSchema(Mapping):
81
- raw_schema: Dict[str, Any] = attr.ib() # pragma: no mutate
82
- location: Optional[str] = attr.ib(default=None) # pragma: no mutate
83
- base_url: Optional[str] = attr.ib(default=None) # pragma: no mutate
84
- method: Optional[Filter] = attr.ib(default=None) # pragma: no mutate
85
- endpoint: Optional[Filter] = attr.ib(default=None) # pragma: no mutate
86
- tag: Optional[Filter] = attr.ib(default=None) # pragma: no mutate
87
- operation_id: Optional[Filter] = attr.ib(default=None) # pragma: no mutate
88
- app: Any = attr.ib(default=None) # pragma: no mutate
89
- hooks: HookDispatcher = attr.ib(factory=lambda: HookDispatcher(scope=HookScope.SCHEMA)) # pragma: no mutate
90
- test_function: Optional[GenericTest] = attr.ib(default=None) # pragma: no mutate
91
- validate_schema: bool = attr.ib(default=True) # pragma: no mutate
92
- skip_deprecated_operations: bool = attr.ib(default=False) # pragma: no mutate
93
- data_generation_methods: Iterable[DataGenerationMethod] = attr.ib(
94
- default=DEFAULT_DATA_GENERATION_METHODS
95
- ) # pragma: no mutate
96
- code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
102
+ raw_schema: dict[str, Any]
103
+ config: ProjectConfig
104
+ location: str | None = None
105
+ filter_set: FilterSet = field(default_factory=FilterSet)
106
+ app: Any = None
107
+ hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
108
+ auth: AuthStorage = field(default_factory=AuthStorage)
109
+ test_function: Callable | None = None
110
+
111
+ def __post_init__(self) -> None:
112
+ self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
113
+
114
+ @property
115
+ def specification(self) -> Specification:
116
+ raise NotImplementedError
117
+
118
+ @property
119
+ def transport(self) -> transport.BaseTransport:
120
+ return transport.get(self.app)
121
+
122
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
123
+
124
+ def include(
125
+ self,
126
+ func: MatcherFunc | None = None,
127
+ *,
128
+ name: FilterValue | None = None,
129
+ name_regex: str | None = None,
130
+ method: FilterValue | None = None,
131
+ method_regex: str | None = None,
132
+ path: FilterValue | None = None,
133
+ path_regex: str | None = None,
134
+ tag: FilterValue | None = None,
135
+ tag_regex: RegexValue | None = None,
136
+ operation_id: FilterValue | None = None,
137
+ operation_id_regex: RegexValue | None = None,
138
+ ) -> BaseSchema:
139
+ """Return a new schema containing only operations matching the specified criteria.
140
+
141
+ Args:
142
+ func: Custom filter function that accepts operation context.
143
+ name: Operation name(s) to include.
144
+ name_regex: Regex pattern for operation names.
145
+ method: HTTP method(s) to include.
146
+ method_regex: Regex pattern for HTTP methods.
147
+ path: API path(s) to include.
148
+ path_regex: Regex pattern for API paths.
149
+ tag: OpenAPI tag(s) to include.
150
+ tag_regex: Regex pattern for OpenAPI tags.
151
+ operation_id: Operation ID(s) to include.
152
+ operation_id_regex: Regex pattern for operation IDs.
153
+
154
+ Returns:
155
+ New schema instance with applied include filters.
156
+
157
+ """
158
+ filter_set = self.filter_set.clone()
159
+ filter_set.include(
160
+ func,
161
+ name=name,
162
+ name_regex=name_regex,
163
+ method=method,
164
+ method_regex=method_regex,
165
+ path=path,
166
+ path_regex=path_regex,
167
+ tag=tag,
168
+ tag_regex=tag_regex,
169
+ operation_id=operation_id,
170
+ operation_id_regex=operation_id_regex,
171
+ )
172
+ return self.clone(filter_set=filter_set)
173
+
174
+ def exclude(
175
+ self,
176
+ func: MatcherFunc | None = None,
177
+ *,
178
+ name: FilterValue | None = None,
179
+ name_regex: str | None = None,
180
+ method: FilterValue | None = None,
181
+ method_regex: str | None = None,
182
+ path: FilterValue | None = None,
183
+ path_regex: str | None = None,
184
+ tag: FilterValue | None = None,
185
+ tag_regex: RegexValue | None = None,
186
+ operation_id: FilterValue | None = None,
187
+ operation_id_regex: RegexValue | None = None,
188
+ deprecated: bool = False,
189
+ ) -> BaseSchema:
190
+ """Return a new schema excluding operations matching the specified criteria.
191
+
192
+ Args:
193
+ func: Custom filter function that accepts operation context.
194
+ name: Operation name(s) to exclude.
195
+ name_regex: Regex pattern for operation names.
196
+ method: HTTP method(s) to exclude.
197
+ method_regex: Regex pattern for HTTP methods.
198
+ path: API path(s) to exclude.
199
+ path_regex: Regex pattern for API paths.
200
+ tag: OpenAPI tag(s) to exclude.
201
+ tag_regex: Regex pattern for OpenAPI tags.
202
+ operation_id: Operation ID(s) to exclude.
203
+ operation_id_regex: Regex pattern for operation IDs.
204
+ deprecated: Whether to exclude deprecated operations.
205
+
206
+ Returns:
207
+ New schema instance with applied exclude filters.
208
+
209
+ """
210
+ filter_set = self.filter_set.clone()
211
+ if deprecated:
212
+ if func is None:
213
+ func = is_deprecated
214
+ else:
215
+ filter_set.exclude(is_deprecated)
216
+ filter_set.exclude(
217
+ func,
218
+ name=name,
219
+ name_regex=name_regex,
220
+ method=method,
221
+ method_regex=method_regex,
222
+ path=path,
223
+ path_regex=path_regex,
224
+ tag=tag,
225
+ tag_regex=tag_regex,
226
+ operation_id=operation_id,
227
+ operation_id_regex=operation_id_regex,
228
+ )
229
+ return self.clone(filter_set=filter_set)
97
230
 
98
231
  def __iter__(self) -> Iterator[str]:
99
- return iter(self.operations)
232
+ raise NotImplementedError
100
233
 
101
- def __getitem__(self, item: str) -> MethodsDict:
234
+ def __getitem__(self, item: str) -> APIOperationMap:
235
+ __tracebackhide__ = True
102
236
  try:
103
- return self.operations[item]
237
+ return self._get_operation_map(item)
104
238
  except KeyError as exc:
105
- matches = get_close_matches(item, list(self.operations))
106
- message = f"`{item}` not found"
107
- if matches:
108
- message += f". Did you mean `{matches[0]}`?"
109
- raise KeyError(message) from exc
239
+ self.on_missing_operation(item, exc)
110
240
 
111
- def __len__(self) -> int:
112
- return len(self.operations)
241
+ def _get_operation_map(self, key: str) -> APIOperationMap:
242
+ raise NotImplementedError
113
243
 
114
- @property # pragma: no mutate
115
- def verbose_name(self) -> str:
244
+ def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
116
245
  raise NotImplementedError
117
246
 
247
+ def __len__(self) -> int:
248
+ return self.statistic.operations.total
249
+
250
+ def hook(self, hook: str | Callable) -> Callable:
251
+ """Register a hook function for this schema only.
252
+
253
+ Args:
254
+ hook: Hook name string or hook function to register.
255
+
256
+ """
257
+ return self.hooks.hook(hook)
258
+
118
259
  def get_full_path(self, path: str) -> str:
119
- """Compute full path for the given path."""
120
260
  return get_full_path(self.base_path, path)
121
261
 
122
262
  @property
@@ -124,8 +264,8 @@ class BaseSchema(Mapping):
124
264
  """Base path for the schema."""
125
265
  # if `base_url` is specified, then it should include base path
126
266
  # Example: http://127.0.0.1:8080/api
127
- if self.base_url:
128
- path = urlsplit(self.base_url).path
267
+ if self.config.base_url:
268
+ path = urlsplit(self.config.base_url).path
129
269
  else:
130
270
  path = self._get_base_path()
131
271
  if not path.endswith("/"):
@@ -140,85 +280,57 @@ class BaseSchema(Mapping):
140
280
  parts = urlsplit(self.location or "")[:2] + (path, "", "")
141
281
  return urlunsplit(parts)
142
282
 
283
+ @cached_property
284
+ def _cached_base_url(self) -> str:
285
+ """Cached base URL computation since schema doesn't change."""
286
+ return self._build_base_url()
287
+
143
288
  def get_base_url(self) -> str:
144
- base_url = self.base_url
289
+ base_url = self.config.base_url
145
290
  if base_url is not None:
146
- return base_url.rstrip("/") # pragma: no mutate
147
- return self._build_base_url()
291
+ return base_url.rstrip("/")
292
+ return self._cached_base_url
148
293
 
149
- @property
150
- def operations(self) -> Dict[str, MethodsDict]:
151
- if not hasattr(self, "_operations"):
152
- # pylint: disable=attribute-defined-outside-init
153
- operations = self.get_all_operations()
154
- self._operations = operations_to_dict(operations)
155
- return self._operations
294
+ def validate(self) -> None:
295
+ raise NotImplementedError
156
296
 
157
- @property
158
- def operations_count(self) -> int:
297
+ @cached_property
298
+ def statistic(self) -> ApiStatistic:
299
+ return self._measure_statistic()
300
+
301
+ def _measure_statistic(self) -> ApiStatistic:
159
302
  raise NotImplementedError
160
303
 
161
304
  def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
162
305
  raise NotImplementedError
163
306
 
164
- def get_strategies_from_examples(self, operation: APIOperation) -> List[SearchStrategy[Case]]:
165
- """Get examples from the API operation."""
307
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
166
308
  raise NotImplementedError
167
309
 
168
- def get_stateful_tests(
169
- self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
170
- ) -> Sequence[StatefulTest]:
171
- """Get a list of additional tests, that should be executed after this response from the API operation."""
310
+ def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
172
311
  raise NotImplementedError
173
312
 
174
- def get_parameter_serializer(self, operation: APIOperation, location: str) -> Optional[Callable]:
175
- """Get a function that serializes parameters for the given location."""
176
- raise NotImplementedError
313
+ def parametrize(self) -> Callable:
314
+ """Return a decorator that marks a test function for `pytest` parametrization.
177
315
 
178
- def get_all_tests(
179
- self,
180
- func: Callable,
181
- settings: Optional[hypothesis.settings] = None,
182
- seed: Optional[int] = None,
183
- _given_kwargs: Optional[Dict[str, GivenInput]] = None,
184
- ) -> Generator[Tuple[Result[Tuple[APIOperation, Callable], InvalidSchema], DataGenerationMethod], None, None]:
185
- """Generate all operations and Hypothesis tests for them."""
186
- for result in self.get_all_operations():
187
- for data_generation_method in self.data_generation_methods:
188
- if isinstance(result, Ok):
189
- test = create_test(
190
- operation=result.ok(),
191
- test=func,
192
- settings=settings,
193
- seed=seed,
194
- data_generation_method=data_generation_method,
195
- _given_kwargs=_given_kwargs,
196
- )
197
- yield Ok((result.ok(), test)), data_generation_method
198
- else:
199
- yield result, data_generation_method
316
+ The decorated test function will be parametrized with test cases generated
317
+ from the schema's API operations.
200
318
 
201
- def parametrize(
202
- self,
203
- method: Optional[Filter] = NOT_SET,
204
- endpoint: Optional[Filter] = NOT_SET,
205
- tag: Optional[Filter] = NOT_SET,
206
- operation_id: Optional[Filter] = NOT_SET,
207
- validate_schema: Union[bool, NotSet] = NOT_SET,
208
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
209
- data_generation_methods: Union[Iterable[DataGenerationMethod], NotSet] = NOT_SET,
210
- code_sample_style: Union[str, NotSet] = NOT_SET,
211
- ) -> Callable:
212
- """Mark a test function as a parametrized one."""
213
- _code_sample_style = (
214
- CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
215
- )
319
+ Returns:
320
+ Decorator function for test parametrization.
321
+
322
+ Raises:
323
+ IncorrectUsage: If applied to the same function multiple times.
324
+
325
+ """
326
+
327
+ def wrapper(func: Callable) -> Callable:
328
+ from schemathesis.pytest.plugin import SchemaHandleMark
216
329
 
217
- def wrapper(func: GenericTest) -> GenericTest:
218
- if hasattr(func, PARAMETRIZE_MARKER):
330
+ if SchemaHandleMark.is_set(func):
219
331
 
220
332
  def wrapped_test(*_: Any, **__: Any) -> NoReturn:
221
- raise UsageError(
333
+ raise IncorrectUsage(
222
334
  f"You have applied `parametrize` to the `{func.__name__}` test more than once, which "
223
335
  "overrides the previous decorator. "
224
336
  "The `parametrize` decorator could be applied to the same function at most once."
@@ -226,92 +338,53 @@ class BaseSchema(Mapping):
226
338
 
227
339
  return wrapped_test
228
340
  HookDispatcher.add_dispatcher(func)
229
- cloned = self.clone(
230
- test_function=func,
231
- method=method,
232
- endpoint=endpoint,
233
- tag=tag,
234
- operation_id=operation_id,
235
- validate_schema=validate_schema,
236
- skip_deprecated_operations=skip_deprecated_operations,
237
- data_generation_methods=data_generation_methods,
238
- code_sample_style=_code_sample_style, # type: ignore
239
- )
240
- setattr(func, PARAMETRIZE_MARKER, cloned)
341
+ cloned = self.clone(test_function=func)
342
+ SchemaHandleMark.set(func, cloned)
241
343
  return func
242
344
 
243
345
  return wrapper
244
346
 
245
347
  def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
246
- """Proxy Hypothesis strategies to ``hypothesis.given``."""
348
+ """Proxy to Hypothesis's `given` decorator for adding custom strategies.
349
+
350
+ Args:
351
+ *args: Positional arguments passed to `hypothesis.given`.
352
+ **kwargs: Keyword arguments passed to `hypothesis.given`.
353
+
354
+ """
247
355
  return given_proxy(*args, **kwargs)
248
356
 
249
357
  def clone(
250
- self,
251
- *,
252
- base_url: Union[Optional[str], NotSet] = NOT_SET,
253
- test_function: Optional[GenericTest] = None,
254
- method: Optional[Filter] = NOT_SET,
255
- endpoint: Optional[Filter] = NOT_SET,
256
- tag: Optional[Filter] = NOT_SET,
257
- operation_id: Optional[Filter] = NOT_SET,
258
- app: Any = NOT_SET,
259
- hooks: Union[HookDispatcher, NotSet] = NOT_SET,
260
- validate_schema: Union[bool, NotSet] = NOT_SET,
261
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
262
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
263
- code_sample_style: Union[CodeSampleStyle, NotSet] = NOT_SET,
264
- ) -> "BaseSchema":
265
- if base_url is NOT_SET:
266
- base_url = self.base_url
267
- if method is NOT_SET:
268
- method = self.method
269
- if endpoint is NOT_SET:
270
- endpoint = self.endpoint
271
- if tag is NOT_SET:
272
- tag = self.tag
273
- if operation_id is NOT_SET:
274
- operation_id = self.operation_id
275
- if app is NOT_SET:
276
- app = self.app
277
- if validate_schema is NOT_SET:
278
- validate_schema = self.validate_schema
279
- if skip_deprecated_operations is NOT_SET:
280
- skip_deprecated_operations = self.skip_deprecated_operations
281
- if hooks is NOT_SET:
282
- hooks = self.hooks
283
- if data_generation_methods is NOT_SET:
284
- data_generation_methods = self.data_generation_methods
285
- if code_sample_style is NOT_SET:
286
- code_sample_style = self.code_sample_style
358
+ self, *, test_function: Callable | NotSet = NOT_SET, filter_set: FilterSet | NotSet = NOT_SET
359
+ ) -> BaseSchema:
360
+ if isinstance(test_function, NotSet):
361
+ _test_function = self.test_function
362
+ else:
363
+ _test_function = test_function
364
+ if isinstance(filter_set, NotSet):
365
+ _filter_set = self.filter_set
366
+ else:
367
+ _filter_set = filter_set
287
368
 
288
369
  return self.__class__(
289
370
  self.raw_schema,
371
+ config=self.config,
290
372
  location=self.location,
291
- base_url=base_url, # type: ignore
292
- method=method,
293
- endpoint=endpoint,
294
- tag=tag,
295
- operation_id=operation_id,
296
- app=app,
297
- hooks=hooks, # type: ignore
298
- test_function=test_function,
299
- validate_schema=validate_schema, # type: ignore
300
- skip_deprecated_operations=skip_deprecated_operations, # type: ignore
301
- data_generation_methods=data_generation_methods, # type: ignore
302
- code_sample_style=code_sample_style, # type: ignore
373
+ app=self.app,
374
+ hooks=self.hooks,
375
+ auth=self.auth,
376
+ test_function=_test_function,
377
+ filter_set=_filter_set,
303
378
  )
304
379
 
305
- def get_local_hook_dispatcher(self) -> Optional[HookDispatcher]:
306
- """Get a HookDispatcher instance bound to the test if present."""
380
+ def get_local_hook_dispatcher(self) -> HookDispatcher | None:
307
381
  # It might be not present when it is used without pytest via `APIOperation.as_strategy()`
308
382
  if self.test_function is not None:
309
383
  # Might be missing it in case of `LazySchema` usage
310
- return getattr(self.test_function, "_schemathesis_hooks", None) # type: ignore
384
+ return HookDispatcherMark.get(self.test_function)
311
385
  return None
312
386
 
313
387
  def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
314
- """Dispatch a hook via all available dispatchers."""
315
388
  dispatch(name, context, *args, **kwargs)
316
389
  self.hooks.dispatch(name, context, *args, **kwargs)
317
390
  local_dispatcher = self.get_local_hook_dispatcher()
@@ -319,63 +392,426 @@ class BaseSchema(Mapping):
319
392
  local_dispatcher.dispatch(name, context, *args, **kwargs)
320
393
 
321
394
  def prepare_multipart(
322
- self, form_data: FormData, operation: APIOperation
323
- ) -> Tuple[Optional[List], Optional[Dict[str, Any]]]:
324
- """Split content of `form_data` into files & data.
325
-
326
- Forms may contain file fields, that we should send via `files` argument in `requests`.
327
- """
395
+ self, form_data: dict[str, Any], operation: APIOperation
396
+ ) -> tuple[list | None, dict[str, Any] | None]:
328
397
  raise NotImplementedError
329
398
 
330
- def get_request_payload_content_types(self, operation: APIOperation) -> List[str]:
399
+ def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
331
400
  raise NotImplementedError
332
401
 
333
402
  def make_case(
334
403
  self,
335
404
  *,
336
- case_cls: Type[C],
337
405
  operation: APIOperation,
338
- path_parameters: Optional[PathParameters] = None,
339
- headers: Optional[Headers] = None,
340
- cookies: Optional[Cookies] = None,
341
- query: Optional[Query] = None,
342
- body: Union[Body, NotSet] = NOT_SET,
343
- media_type: Optional[str] = None,
344
- ) -> C:
406
+ method: str | None = None,
407
+ path: str | None = None,
408
+ path_parameters: dict[str, Any] | None = None,
409
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
410
+ cookies: dict[str, Any] | None = None,
411
+ query: dict[str, Any] | None = None,
412
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
413
+ media_type: str | None = None,
414
+ meta: CaseMetadata | None = None,
415
+ ) -> Case:
345
416
  raise NotImplementedError
346
417
 
347
418
  def get_case_strategy(
348
419
  self,
349
420
  operation: APIOperation,
350
- hooks: Optional[HookDispatcher] = None,
351
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
421
+ hooks: HookDispatcher | None = None,
422
+ auth_storage: AuthStorage | None = None,
423
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
424
+ **kwargs: Any,
352
425
  ) -> SearchStrategy:
353
426
  raise NotImplementedError
354
427
 
355
- def as_state_machine(self) -> Type[APIStateMachine]:
356
- """Create a state machine class.
428
+ def as_state_machine(self) -> type[APIStateMachine]:
429
+ """Create a state machine class for stateful testing of linked API operations.
430
+
431
+ Returns:
432
+ APIStateMachine subclass configured for this schema.
357
433
 
358
- Use it for stateful testing.
359
434
  """
360
435
  raise NotImplementedError
361
436
 
362
- def get_links(self, operation: APIOperation) -> Dict[str, Dict[str, Any]]:
437
+ def get_tags(self, operation: APIOperation) -> list[str] | None:
363
438
  raise NotImplementedError
364
439
 
365
- def validate_response(self, operation: APIOperation, response: GenericResponse) -> None:
440
+ def validate_response(
441
+ self,
442
+ operation: APIOperation,
443
+ response: Response,
444
+ *,
445
+ case: Case | None = None,
446
+ ) -> bool | None:
366
447
  raise NotImplementedError
367
448
 
368
- def prepare_schema(self, schema: Any) -> Any:
449
+ def as_strategy(
450
+ self,
451
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
452
+ **kwargs: Any,
453
+ ) -> SearchStrategy:
454
+ """Create a Hypothesis strategy that generates test cases for all schema operations.
455
+
456
+ Use with `@given` in non-Schemathesis tests.
457
+
458
+ Args:
459
+ generation_mode: Whether to generate positive or negative test data.
460
+ **kwargs: Additional keywords for each strategy.
461
+
462
+ Returns:
463
+ Combined Hypothesis strategy for all valid operations in the schema.
464
+
465
+ """
466
+ from hypothesis import strategies as st
467
+
468
+ _strategies = [
469
+ operation.ok().as_strategy(generation_mode=generation_mode, **kwargs)
470
+ for operation in self.get_all_operations()
471
+ if isinstance(operation, Ok)
472
+ ]
473
+ return st.one_of(_strategies)
474
+
475
+ def find_operation_by_label(self, label: str) -> APIOperation | None:
369
476
  raise NotImplementedError
370
477
 
371
478
 
372
- def operations_to_dict(
373
- operations: Generator[Result[APIOperation, InvalidSchema], None, None]
374
- ) -> Dict[str, MethodsDict]:
375
- output: Dict[str, MethodsDict] = {}
376
- for result in operations:
377
- if isinstance(result, Ok):
378
- operation = result.ok()
379
- output.setdefault(operation.path, MethodsDict())
380
- output[operation.path][operation.method] = operation
381
- return output
479
+ @dataclass
480
+ class APIOperationMap(Mapping):
481
+ _schema: BaseSchema
482
+ _data: Mapping
483
+
484
+ __slots__ = ("_schema", "_data")
485
+
486
+ def __getitem__(self, item: str) -> APIOperation:
487
+ return self._data[item]
488
+
489
+ def __len__(self) -> int:
490
+ return len(self._data)
491
+
492
+ def __iter__(self) -> Iterator[str]:
493
+ return iter(self._data)
494
+
495
+ def as_strategy(
496
+ self,
497
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
498
+ **kwargs: Any,
499
+ ) -> SearchStrategy:
500
+ """Create a Hypothesis strategy that generates test cases for all schema operations in this subset.
501
+
502
+ Use with `@given` in non-Schemathesis tests.
503
+
504
+ Args:
505
+ generation_mode: Whether to generate positive or negative test data.
506
+ **kwargs: Additional keywords for each strategy.
507
+
508
+ Returns:
509
+ Combined Hypothesis strategy for all valid operations in the schema.
510
+
511
+ """
512
+ from hypothesis import strategies as st
513
+
514
+ _strategies = [
515
+ operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
516
+ ]
517
+ return st.one_of(_strategies)
518
+
519
+
520
+ P = TypeVar("P", bound=OperationParameter)
521
+
522
+
523
+ @dataclass
524
+ class ParameterSet(Generic[P]):
525
+ """A set of parameters for the same location."""
526
+
527
+ items: list[P]
528
+
529
+ __slots__ = ("items",)
530
+
531
+ def __init__(self, items: list[P] | None = None) -> None:
532
+ self.items = items or []
533
+
534
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
535
+
536
+ def add(self, parameter: P) -> None:
537
+ """Add a new parameter."""
538
+ self.items.append(parameter)
539
+
540
+ def get(self, name: str) -> P | None:
541
+ for parameter in self:
542
+ if parameter.name == name:
543
+ return parameter
544
+ return None
545
+
546
+ def __contains__(self, name: str) -> bool:
547
+ for parameter in self.items:
548
+ if parameter.name == name:
549
+ return True
550
+ return False
551
+
552
+ def __iter__(self) -> Generator[P, None, None]:
553
+ yield from iter(self.items)
554
+
555
+ def __len__(self) -> int:
556
+ return len(self.items)
557
+
558
+ def __getitem__(self, item: int) -> P:
559
+ return self.items[item]
560
+
561
+
562
+ class PayloadAlternatives(ParameterSet[P]):
563
+ """A set of alternative payloads."""
564
+
565
+
566
+ R = TypeVar("R", bound=ResponsesContainer)
567
+ S = TypeVar("S")
568
+ D = TypeVar("D", bound=dict)
569
+
570
+
571
+ @dataclass(repr=False)
572
+ class OperationDefinition(Generic[D]):
573
+ """A wrapper to store not resolved API operation definitions.
574
+
575
+ To prevent recursion errors we need to store definitions without resolving references. But operation definitions
576
+ itself can be behind a reference (when there is a ``$ref`` in ``paths`` values), therefore we need to store this
577
+ scope change to have a proper reference resolving later.
578
+ """
579
+
580
+ raw: D
581
+
582
+ __slots__ = ("raw",)
583
+
584
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
585
+
586
+
587
+ @dataclass()
588
+ class APIOperation(Generic[P, R, S]):
589
+ """An API operation (e.g., `GET /users`)."""
590
+
591
+ # `path` does not contain `basePath`
592
+ # Example <scheme>://<host>/<basePath>/users - "/users" is path
593
+ # https://swagger.io/docs/specification/2-0/api-host-and-base-path/
594
+ path: str
595
+ method: str
596
+ definition: OperationDefinition = field(repr=False)
597
+ schema: BaseSchema
598
+ responses: R
599
+ security: S
600
+ label: str = None # type: ignore[assignment]
601
+ app: Any = None
602
+ base_url: str | None = None
603
+ path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
604
+ headers: ParameterSet[P] = field(default_factory=ParameterSet)
605
+ cookies: ParameterSet[P] = field(default_factory=ParameterSet)
606
+ query: ParameterSet[P] = field(default_factory=ParameterSet)
607
+ body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
608
+
609
+ def __post_init__(self) -> None:
610
+ if self.label is None:
611
+ self.label = f"{self.method.upper()} {self.path}" # type: ignore[unreachable]
612
+
613
+ def __deepcopy__(self, memo: dict) -> APIOperation[P, R, S]:
614
+ return self
615
+
616
+ def __hash__(self) -> int:
617
+ return hash(self.label)
618
+
619
+ def __eq__(self, value: object, /) -> bool:
620
+ if not isinstance(value, APIOperation):
621
+ return NotImplemented
622
+ return self.label == value.label
623
+
624
+ @property
625
+ def full_path(self) -> str:
626
+ return self.schema.get_full_path(self.path)
627
+
628
+ @property
629
+ def tags(self) -> list[str] | None:
630
+ return self.schema.get_tags(self)
631
+
632
+ def iter_parameters(self) -> Iterator[P]:
633
+ return chain(self.path_parameters, self.headers, self.cookies, self.query)
634
+
635
+ def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
636
+ return {
637
+ "path": self.path_parameters,
638
+ "header": self.headers,
639
+ "cookie": self.cookies,
640
+ "query": self.query,
641
+ "body": self.body,
642
+ }.get(location)
643
+
644
+ def add_parameter(self, parameter: P) -> None:
645
+ # If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
646
+ # But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
647
+ # In this case, we still might generate some tests for an API operation, but without this parameter,
648
+ # which is better than skip the whole operation from testing.
649
+ container = self._lookup_container(parameter.location)
650
+ if container is not None:
651
+ container.add(parameter)
652
+
653
+ def get_parameter(self, name: str, location: str) -> P | None:
654
+ container = self._lookup_container(location)
655
+ if container is not None:
656
+ return container.get(name)
657
+ return None
658
+
659
+ def get_bodies_for_media_type(self, media_type: str) -> Iterator[P]:
660
+ main_target, sub_target = media_types.parse(media_type)
661
+ for body in self.body:
662
+ main, sub = media_types.parse(body.media_type) # type:ignore[attr-defined]
663
+ if main in ("*", main_target) and sub in ("*", sub_target):
664
+ yield body
665
+
666
+ def as_strategy(
667
+ self,
668
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
669
+ **kwargs: Any,
670
+ ) -> SearchStrategy[Case]:
671
+ """Create a Hypothesis strategy that generates test cases for this API operation.
672
+
673
+ Use with `@given` in non-Schemathesis tests.
674
+
675
+ Args:
676
+ generation_mode: Whether to generate positive or negative test data.
677
+ **kwargs: Extra arguments to the underlying strategy function.
678
+
679
+ """
680
+ if self.schema.config.headers:
681
+ headers = kwargs.setdefault("headers", {})
682
+ headers.update(self.schema.config.headers)
683
+ strategy = self.schema.get_case_strategy(self, generation_mode=generation_mode, **kwargs)
684
+
685
+ def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
686
+ context = HookContext(operation=self)
687
+ for hook in dispatcher.get_all_by_name("before_generate_case"):
688
+ if _should_skip_hook(hook, context):
689
+ continue
690
+ _strategy = hook(context, _strategy)
691
+ for hook in dispatcher.get_all_by_name("filter_case"):
692
+ if _should_skip_hook(hook, context):
693
+ continue
694
+ hook = partial(hook, context)
695
+ _strategy = _strategy.filter(hook)
696
+ for hook in dispatcher.get_all_by_name("map_case"):
697
+ if _should_skip_hook(hook, context):
698
+ continue
699
+ hook = partial(hook, context)
700
+ _strategy = _strategy.map(hook)
701
+ for hook in dispatcher.get_all_by_name("flatmap_case"):
702
+ if _should_skip_hook(hook, context):
703
+ continue
704
+ hook = partial(hook, context)
705
+ _strategy = _strategy.flatmap(hook)
706
+ return _strategy
707
+
708
+ strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
709
+ strategy = _apply_hooks(self.schema.hooks, strategy)
710
+ hooks = kwargs.get("hooks")
711
+ if hooks is not None:
712
+ strategy = _apply_hooks(hooks, strategy)
713
+ return strategy
714
+
715
+ def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
716
+ return self.schema.get_strategies_from_examples(self, **kwargs)
717
+
718
+ def get_parameter_serializer(self, location: str) -> Callable | None:
719
+ return self.schema.get_parameter_serializer(self, location)
720
+
721
+ def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
722
+ return self.schema.prepare_multipart(form_data, self)
723
+
724
+ def get_request_payload_content_types(self) -> list[str]:
725
+ return self.schema.get_request_payload_content_types(self)
726
+
727
+ def _get_default_media_type(self) -> str:
728
+ # If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
729
+ media_types = self.get_request_payload_content_types()
730
+ if len(media_types) == 1:
731
+ # The only available option
732
+ return media_types[0]
733
+ media_types_repr = ", ".join(media_types)
734
+ raise IncorrectUsage(
735
+ "Can not detect appropriate media type. "
736
+ "You can either specify one of the defined media types "
737
+ f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
738
+ )
739
+
740
+ def validate_response(
741
+ self,
742
+ response: Response | httpx.Response | requests.Response | TestResponse,
743
+ *,
744
+ case: Case | None = None,
745
+ ) -> bool | None:
746
+ """Validate a response against the API schema.
747
+
748
+ Args:
749
+ response: The HTTP response to validate. Can be a `requests.Response`,
750
+ `httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
751
+ case: The generated test case related to the provided response.
752
+
753
+ Raises:
754
+ FailureGroup: If the response does not conform to the schema.
755
+
756
+ """
757
+ return self.schema.validate_response(self, Response.from_any(response), case=case)
758
+
759
+ def is_valid_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool:
760
+ """Check if the provided response is valid against the API schema.
761
+
762
+ Args:
763
+ response: The HTTP response to validate. Can be a `requests.Response`,
764
+ `httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
765
+
766
+ Returns:
767
+ `True` if response is valid, `False` otherwise.
768
+
769
+ """
770
+ try:
771
+ self.validate_response(response)
772
+ return True
773
+ except AssertionError:
774
+ return False
775
+
776
+ def Case(
777
+ self,
778
+ *,
779
+ method: str | None = None,
780
+ path_parameters: dict[str, Any] | None = None,
781
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
782
+ cookies: dict[str, Any] | None = None,
783
+ query: dict[str, Any] | None = None,
784
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
785
+ media_type: str | None = None,
786
+ _meta: CaseMetadata | None = None,
787
+ ) -> Case:
788
+ """Create a test case with specific data instead of generated values.
789
+
790
+ Args:
791
+ method: Override HTTP method.
792
+ path_parameters: Override path variables.
793
+ headers: Override HTTP headers.
794
+ cookies: Override cookies.
795
+ query: Override query parameters.
796
+ body: Override request body.
797
+ media_type: Override media type.
798
+
799
+ """
800
+ from requests.structures import CaseInsensitiveDict
801
+
802
+ return self.schema.make_case(
803
+ operation=self,
804
+ method=method,
805
+ path_parameters=path_parameters or {},
806
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
807
+ cookies=cookies or {},
808
+ query=query or {},
809
+ body=body,
810
+ media_type=media_type,
811
+ meta=_meta,
812
+ )
813
+
814
+ @property
815
+ def operation_reference(self) -> str:
816
+ path = self.path.replace("~", "~0").replace("/", "~1")
817
+ return f"#/paths/{path}/{self.method}"