schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py CHANGED
@@ -1,65 +1,52 @@
1
- """Schema objects provide a convenient interface to raw schemas.
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
1
  from __future__ import annotations
10
- from collections.abc import Mapping, MutableMapping
11
- from contextlib import nullcontext
2
+
3
+ from collections.abc import Mapping
12
4
  from dataclasses import dataclass, field
13
- from functools import lru_cache
5
+ from functools import lru_cache, partial
6
+ from itertools import chain
14
7
  from typing import (
8
+ TYPE_CHECKING,
15
9
  Any,
16
10
  Callable,
17
- ContextManager,
18
11
  Generator,
19
- Iterable,
12
+ Generic,
20
13
  Iterator,
21
14
  NoReturn,
22
- Sequence,
23
15
  TypeVar,
24
- TYPE_CHECKING,
25
16
  )
26
- from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
27
-
28
- import hypothesis
29
- from hypothesis.strategies import SearchStrategy
30
- from pyrate_limiter import Limiter
17
+ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
18
+
19
+ from schemathesis import transport
20
+ from schemathesis.core import NOT_SET, NotSet
21
+ from schemathesis.core.errors import IncorrectUsage, InvalidSchema
22
+ from schemathesis.core.output import OutputConfig
23
+ from schemathesis.core.rate_limit import build_limiter
24
+ from schemathesis.core.result import Ok, Result
25
+ from schemathesis.core.transport import Response
26
+ from schemathesis.generation import GenerationConfig, GenerationMode
27
+ from schemathesis.generation.case import Case
28
+ from schemathesis.generation.hypothesis import strategies
29
+ from schemathesis.generation.hypothesis.given import GivenInput, given_proxy
30
+ from schemathesis.generation.meta import CaseMetadata
31
+ from schemathesis.hooks import HookDispatcherMark
31
32
 
32
- from .constants import NOT_SET
33
- from ._hypothesis import create_test
34
33
  from .auths import AuthStorage
35
- from .code_samples import CodeSampleStyle
36
- from .generation import (
37
- DEFAULT_DATA_GENERATION_METHODS,
38
- DataGenerationMethod,
39
- DataGenerationMethodInput,
40
- GenerationConfig,
34
+ from .filters import (
35
+ FilterSet,
36
+ FilterValue,
37
+ MatcherFunc,
38
+ RegexValue,
39
+ is_deprecated,
41
40
  )
42
- from .exceptions import OperationSchemaError, UsageError
43
- from .hooks import HookContext, HookDispatcher, HookScope, dispatch
44
- from .internal.result import Result, Ok
45
- from .models import APIOperation, Case
46
- from .stateful.state_machine import APIStateMachine
47
- from .stateful import Stateful, StatefulTest
48
- from .types import (
49
- Body,
50
- Cookies,
51
- Filter,
52
- FormData,
53
- GenericTest,
54
- Headers,
55
- NotSet,
56
- PathParameters,
57
- Query,
58
- )
59
- from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy, combine_strategies
41
+ from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
60
42
 
61
43
  if TYPE_CHECKING:
62
- from .transports.responses import GenericResponse
44
+ from hypothesis.strategies import SearchStrategy
45
+ from pyrate_limiter import Limiter
46
+ from typing_extensions import Self
47
+
48
+ from schemathesis.core import Specification
49
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
63
50
 
64
51
 
65
52
  C = TypeVar("C", bound=Case)
@@ -70,52 +57,140 @@ def get_full_path(base_path: str, path: str) -> str:
70
57
  return unquote(urljoin(base_path, quote(path.lstrip("/"))))
71
58
 
72
59
 
60
+ @dataclass
61
+ class ApiOperationsCount:
62
+ """Statistics about API operations."""
63
+
64
+ total: int
65
+ selected: int
66
+
67
+ __slots__ = ("total", "selected")
68
+
69
+ def __init__(self) -> None:
70
+ self.total = 0
71
+ self.selected = 0
72
+
73
+
73
74
  @dataclass(eq=False)
74
75
  class BaseSchema(Mapping):
75
76
  raw_schema: dict[str, Any]
76
77
  location: str | None = None
77
78
  base_url: str | None = None
78
- method: Filter | None = None
79
- endpoint: Filter | None = None
80
- tag: Filter | None = None
81
- operation_id: Filter | None = None
79
+ filter_set: FilterSet = field(default_factory=FilterSet)
82
80
  app: Any = None
83
81
  hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
84
82
  auth: AuthStorage = field(default_factory=AuthStorage)
85
- test_function: GenericTest | None = None
86
- validate_schema: bool = True
87
- skip_deprecated_operations: bool = False
88
- data_generation_methods: list[DataGenerationMethod] = field(
89
- default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
90
- )
83
+ test_function: Callable | None = None
91
84
  generation_config: GenerationConfig = field(default_factory=GenerationConfig)
92
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
85
+ output_config: OutputConfig = field(default_factory=OutputConfig)
93
86
  rate_limiter: Limiter | None = None
94
- sanitize_output: bool = True
87
+ _operations_count: ApiOperationsCount | None = None
88
+
89
+ def __post_init__(self) -> None:
90
+ self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
91
+
92
+ @property
93
+ def specification(self) -> Specification:
94
+ raise NotImplementedError
95
+
96
+ @property
97
+ def transport(self) -> transport.BaseTransport:
98
+ return transport.get(self.app)
99
+
100
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
101
+
102
+ def include(
103
+ self,
104
+ func: MatcherFunc | None = None,
105
+ *,
106
+ name: FilterValue | None = None,
107
+ name_regex: str | None = None,
108
+ method: FilterValue | None = None,
109
+ method_regex: str | None = None,
110
+ path: FilterValue | None = None,
111
+ path_regex: str | None = None,
112
+ tag: FilterValue | None = None,
113
+ tag_regex: RegexValue | None = None,
114
+ operation_id: FilterValue | None = None,
115
+ operation_id_regex: RegexValue | None = None,
116
+ ) -> BaseSchema:
117
+ """Include only operations that match the given filters."""
118
+ filter_set = self.filter_set.clone()
119
+ filter_set.include(
120
+ func,
121
+ name=name,
122
+ name_regex=name_regex,
123
+ method=method,
124
+ method_regex=method_regex,
125
+ path=path,
126
+ path_regex=path_regex,
127
+ tag=tag,
128
+ tag_regex=tag_regex,
129
+ operation_id=operation_id,
130
+ operation_id_regex=operation_id_regex,
131
+ )
132
+ return self.clone(filter_set=filter_set)
133
+
134
+ def exclude(
135
+ self,
136
+ func: MatcherFunc | None = None,
137
+ *,
138
+ name: FilterValue | None = None,
139
+ name_regex: str | None = None,
140
+ method: FilterValue | None = None,
141
+ method_regex: str | None = None,
142
+ path: FilterValue | None = None,
143
+ path_regex: str | None = None,
144
+ tag: FilterValue | None = None,
145
+ tag_regex: RegexValue | None = None,
146
+ operation_id: FilterValue | None = None,
147
+ operation_id_regex: RegexValue | None = None,
148
+ deprecated: bool = False,
149
+ ) -> BaseSchema:
150
+ """Include only operations that match the given filters."""
151
+ filter_set = self.filter_set.clone()
152
+ if deprecated:
153
+ if func is None:
154
+ func = is_deprecated
155
+ else:
156
+ filter_set.exclude(is_deprecated)
157
+ filter_set.exclude(
158
+ func,
159
+ name=name,
160
+ name_regex=name_regex,
161
+ method=method,
162
+ method_regex=method_regex,
163
+ path=path,
164
+ path_regex=path_regex,
165
+ tag=tag,
166
+ tag_regex=tag_regex,
167
+ operation_id=operation_id,
168
+ operation_id_regex=operation_id_regex,
169
+ )
170
+ return self.clone(filter_set=filter_set)
95
171
 
96
172
  def __iter__(self) -> Iterator[str]:
97
- return iter(self.operations)
173
+ raise NotImplementedError
98
174
 
99
175
  def __getitem__(self, item: str) -> APIOperationMap:
100
176
  __tracebackhide__ = True
101
177
  try:
102
- return self.operations[item]
178
+ return self._get_operation_map(item)
103
179
  except KeyError as exc:
104
180
  self.on_missing_operation(item, exc)
105
181
 
182
+ def _get_operation_map(self, key: str) -> APIOperationMap:
183
+ raise NotImplementedError
184
+
106
185
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
107
186
  raise NotImplementedError
108
187
 
109
188
  def __len__(self) -> int:
110
- return len(self.operations)
189
+ return self.count_operations().total
111
190
 
112
191
  def hook(self, hook: str | Callable) -> Callable:
113
192
  return self.hooks.register(hook)
114
193
 
115
- @property
116
- def verbose_name(self) -> str:
117
- raise NotImplementedError
118
-
119
194
  def get_full_path(self, path: str) -> str:
120
195
  """Compute full path for the given path."""
121
196
  return get_full_path(self.base_path, path)
@@ -150,20 +225,14 @@ class BaseSchema(Mapping):
150
225
  def validate(self) -> None:
151
226
  raise NotImplementedError
152
227
 
153
- @property
154
- def operations(self) -> dict[str, APIOperationMap]:
155
- if not hasattr(self, "_operations"):
156
- operations = self.get_all_operations()
157
- self._operations = self._store_operations(operations)
158
- return self._operations
159
-
160
- def _store_operations(
161
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
162
- ) -> dict[str, APIOperationMap]:
163
- raise NotImplementedError
228
+ def count_operations(self) -> ApiOperationsCount:
229
+ """Count total and selected operations."""
230
+ if self._operations_count is None:
231
+ self._operations_count = self._do_count_operations()
232
+ return self._operations_count
164
233
 
165
- @property
166
- def operations_count(self) -> int:
234
+ def _do_count_operations(self) -> ApiOperationsCount:
235
+ """Implementation-specific counting logic."""
167
236
  raise NotImplementedError
168
237
 
169
238
  @property
@@ -171,11 +240,11 @@ class BaseSchema(Mapping):
171
240
  raise NotImplementedError
172
241
 
173
242
  def get_all_operations(
174
- self, hooks: HookDispatcher | None = None
175
- ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
243
+ self, generation_config: GenerationConfig | None = None
244
+ ) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
176
245
  raise NotImplementedError
177
246
 
178
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
247
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
179
248
  """Get examples from the API operation."""
180
249
  raise NotImplementedError
181
250
 
@@ -183,70 +252,20 @@ class BaseSchema(Mapping):
183
252
  """Get applied security requirements for the given API operation."""
184
253
  raise NotImplementedError
185
254
 
186
- def get_stateful_tests(
187
- self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
188
- ) -> Sequence[StatefulTest]:
189
- """Get a list of additional tests, that should be executed after this response from the API operation."""
190
- raise NotImplementedError
191
-
192
255
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
193
256
  """Get a function that serializes parameters for the given location."""
194
257
  raise NotImplementedError
195
258
 
196
- def get_all_tests(
197
- self,
198
- func: Callable,
199
- settings: hypothesis.settings | None = None,
200
- generation_config: GenerationConfig | None = None,
201
- seed: int | None = None,
202
- as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None = None,
203
- hooks: HookDispatcher | None = None,
204
- _given_kwargs: dict[str, GivenInput] | None = None,
205
- ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
206
- """Generate all operations and Hypothesis tests for them."""
207
- for result in self.get_all_operations(hooks=hooks):
208
- if isinstance(result, Ok):
209
- operation = result.ok()
210
- _as_strategy_kwargs: dict[str, Any] | None
211
- if callable(as_strategy_kwargs):
212
- _as_strategy_kwargs = as_strategy_kwargs(operation)
213
- else:
214
- _as_strategy_kwargs = as_strategy_kwargs
215
- test = create_test(
216
- operation=operation,
217
- test=func,
218
- settings=settings,
219
- seed=seed,
220
- data_generation_methods=self.data_generation_methods,
221
- generation_config=generation_config,
222
- as_strategy_kwargs=_as_strategy_kwargs,
223
- _given_kwargs=_given_kwargs,
224
- )
225
- yield Ok((operation, test))
226
- else:
227
- yield result
228
-
229
- def parametrize(
230
- self,
231
- method: Filter | None = NOT_SET,
232
- endpoint: Filter | None = NOT_SET,
233
- tag: Filter | None = NOT_SET,
234
- operation_id: Filter | None = NOT_SET,
235
- validate_schema: bool | NotSet = NOT_SET,
236
- skip_deprecated_operations: bool | NotSet = NOT_SET,
237
- data_generation_methods: Iterable[DataGenerationMethod] | NotSet = NOT_SET,
238
- code_sample_style: str | NotSet = NOT_SET,
239
- ) -> Callable:
259
+ def parametrize(self) -> Callable:
240
260
  """Mark a test function as a parametrized one."""
241
- _code_sample_style = (
242
- CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
243
- )
244
261
 
245
- def wrapper(func: GenericTest) -> GenericTest:
246
- if hasattr(func, PARAMETRIZE_MARKER):
262
+ def wrapper(func: Callable) -> Callable:
263
+ from schemathesis.pytest.plugin import SchemaHandleMark
264
+
265
+ if SchemaHandleMark.is_set(func):
247
266
 
248
267
  def wrapped_test(*_: Any, **__: Any) -> NoReturn:
249
- raise UsageError(
268
+ raise IncorrectUsage(
250
269
  f"You have applied `parametrize` to the `{func.__name__}` test more than once, which "
251
270
  "overrides the previous decorator. "
252
271
  "The `parametrize` decorator could be applied to the same function at most once."
@@ -254,18 +273,8 @@ class BaseSchema(Mapping):
254
273
 
255
274
  return wrapped_test
256
275
  HookDispatcher.add_dispatcher(func)
257
- cloned = self.clone(
258
- test_function=func,
259
- method=method,
260
- endpoint=endpoint,
261
- tag=tag,
262
- operation_id=operation_id,
263
- validate_schema=validate_schema,
264
- skip_deprecated_operations=skip_deprecated_operations,
265
- data_generation_methods=data_generation_methods,
266
- code_sample_style=_code_sample_style, # type: ignore
267
- )
268
- setattr(func, PARAMETRIZE_MARKER, cloned)
276
+ cloned = self.clone(test_function=func)
277
+ SchemaHandleMark.set(func, cloned)
269
278
  return func
270
279
 
271
280
  return wrapper
@@ -275,75 +284,29 @@ class BaseSchema(Mapping):
275
284
  return given_proxy(*args, **kwargs)
276
285
 
277
286
  def clone(
278
- self,
279
- *,
280
- base_url: str | None | NotSet = NOT_SET,
281
- test_function: GenericTest | None = None,
282
- method: Filter | None = NOT_SET,
283
- endpoint: Filter | None = NOT_SET,
284
- tag: Filter | None = NOT_SET,
285
- operation_id: Filter | None = NOT_SET,
286
- app: Any = NOT_SET,
287
- hooks: HookDispatcher | NotSet = NOT_SET,
288
- auth: AuthStorage | NotSet = NOT_SET,
289
- validate_schema: bool | NotSet = NOT_SET,
290
- skip_deprecated_operations: bool | NotSet = NOT_SET,
291
- data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
292
- generation_config: GenerationConfig | NotSet = NOT_SET,
293
- code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
294
- rate_limiter: Limiter | None = NOT_SET,
295
- sanitize_output: bool | NotSet | None = NOT_SET,
287
+ self, *, test_function: Callable | NotSet = NOT_SET, filter_set: FilterSet | NotSet = NOT_SET
296
288
  ) -> BaseSchema:
297
- if base_url is NOT_SET:
298
- base_url = self.base_url
299
- if method is NOT_SET:
300
- method = self.method
301
- if endpoint is NOT_SET:
302
- endpoint = self.endpoint
303
- if tag is NOT_SET:
304
- tag = self.tag
305
- if operation_id is NOT_SET:
306
- operation_id = self.operation_id
307
- if app is NOT_SET:
308
- app = self.app
309
- if validate_schema is NOT_SET:
310
- validate_schema = self.validate_schema
311
- if skip_deprecated_operations is NOT_SET:
312
- skip_deprecated_operations = self.skip_deprecated_operations
313
- if hooks is NOT_SET:
314
- hooks = self.hooks
315
- if auth is NOT_SET:
316
- auth = self.auth
317
- if data_generation_methods is NOT_SET:
318
- data_generation_methods = self.data_generation_methods
319
- if generation_config is NOT_SET:
320
- generation_config = self.generation_config
321
- if code_sample_style is NOT_SET:
322
- code_sample_style = self.code_sample_style
323
- if rate_limiter is NOT_SET:
324
- rate_limiter = self.rate_limiter
325
- if sanitize_output is NOT_SET:
326
- sanitize_output = self.sanitize_output
289
+ if isinstance(test_function, NotSet):
290
+ _test_function = self.test_function
291
+ else:
292
+ _test_function = test_function
293
+ if isinstance(filter_set, NotSet):
294
+ _filter_set = self.filter_set
295
+ else:
296
+ _filter_set = filter_set
327
297
 
328
298
  return self.__class__(
329
299
  self.raw_schema,
330
300
  location=self.location,
331
- base_url=base_url, # type: ignore
332
- method=method,
333
- endpoint=endpoint,
334
- tag=tag,
335
- operation_id=operation_id,
336
- app=app,
337
- hooks=hooks, # type: ignore
338
- auth=auth, # type: ignore
339
- test_function=test_function,
340
- validate_schema=validate_schema, # type: ignore
341
- skip_deprecated_operations=skip_deprecated_operations, # type: ignore
342
- data_generation_methods=data_generation_methods, # type: ignore
343
- generation_config=generation_config, # type: ignore
344
- code_sample_style=code_sample_style, # type: ignore
345
- rate_limiter=rate_limiter, # type: ignore
346
- sanitize_output=sanitize_output, # type: ignore
301
+ base_url=self.base_url,
302
+ app=self.app,
303
+ hooks=self.hooks,
304
+ auth=self.auth,
305
+ test_function=_test_function,
306
+ generation_config=self.generation_config,
307
+ output_config=self.output_config,
308
+ rate_limiter=self.rate_limiter,
309
+ filter_set=_filter_set,
347
310
  )
348
311
 
349
312
  def get_local_hook_dispatcher(self) -> HookDispatcher | None:
@@ -351,7 +314,7 @@ class BaseSchema(Mapping):
351
314
  # It might be not present when it is used without pytest via `APIOperation.as_strategy()`
352
315
  if self.test_function is not None:
353
316
  # Might be missing it in case of `LazySchema` usage
354
- return getattr(self.test_function, "_schemathesis_hooks", None) # type: ignore
317
+ return HookDispatcherMark.get(self.test_function)
355
318
  return None
356
319
 
357
320
  def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
@@ -363,7 +326,7 @@ class BaseSchema(Mapping):
363
326
  local_dispatcher.dispatch(name, context, *args, **kwargs)
364
327
 
365
328
  def prepare_multipart(
366
- self, form_data: FormData, operation: APIOperation
329
+ self, form_data: dict[str, Any], operation: APIOperation
367
330
  ) -> tuple[list | None, dict[str, Any] | None]:
368
331
  """Split content of `form_data` into files & data.
369
332
 
@@ -377,15 +340,17 @@ class BaseSchema(Mapping):
377
340
  def make_case(
378
341
  self,
379
342
  *,
380
- case_cls: type[C],
381
343
  operation: APIOperation,
382
- path_parameters: PathParameters | None = None,
383
- headers: Headers | None = None,
384
- cookies: Cookies | None = None,
385
- query: Query | None = None,
386
- body: Body | NotSet = NOT_SET,
344
+ method: str | None = None,
345
+ path: str | None = None,
346
+ path_parameters: dict[str, Any] | None = None,
347
+ headers: dict[str, Any] | None = None,
348
+ cookies: dict[str, Any] | None = None,
349
+ query: dict[str, Any] | None = None,
350
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
387
351
  media_type: str | None = None,
388
- ) -> C:
352
+ meta: CaseMetadata | None = None,
353
+ ) -> Case:
389
354
  raise NotImplementedError
390
355
 
391
356
  def get_case_strategy(
@@ -393,17 +358,14 @@ class BaseSchema(Mapping):
393
358
  operation: APIOperation,
394
359
  hooks: HookDispatcher | None = None,
395
360
  auth_storage: AuthStorage | None = None,
396
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
361
+ generation_mode: GenerationMode = GenerationMode.default(),
397
362
  generation_config: GenerationConfig | None = None,
398
363
  **kwargs: Any,
399
364
  ) -> SearchStrategy:
400
365
  raise NotImplementedError
401
366
 
402
367
  def as_state_machine(self) -> type[APIStateMachine]:
403
- """Create a state machine class.
404
-
405
- Use it for stateful testing.
406
- """
368
+ """Create a state machine class."""
407
369
  raise NotImplementedError
408
370
 
409
371
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
@@ -412,19 +374,12 @@ class BaseSchema(Mapping):
412
374
  def get_tags(self, operation: APIOperation) -> list[str] | None:
413
375
  raise NotImplementedError
414
376
 
415
- def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
377
+ def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
416
378
  raise NotImplementedError
417
379
 
418
380
  def prepare_schema(self, schema: Any) -> Any:
419
381
  raise NotImplementedError
420
382
 
421
- def ratelimit(self) -> ContextManager:
422
- """Limit the rate of sending generated requests."""
423
- label = urlparse(self.base_url).netloc
424
- if self.rate_limiter is not None:
425
- return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
426
- return nullcontext()
427
-
428
383
  def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
429
384
  raise NotImplementedError
430
385
 
@@ -432,63 +387,384 @@ class BaseSchema(Mapping):
432
387
  self,
433
388
  hooks: HookDispatcher | None = None,
434
389
  auth_storage: AuthStorage | None = None,
435
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
390
+ generation_mode: GenerationMode = GenerationMode.default(),
436
391
  generation_config: GenerationConfig | None = None,
437
392
  **kwargs: Any,
438
393
  ) -> SearchStrategy:
439
394
  """Build a strategy for generating test cases for all defined API operations."""
440
- assert len(self.operations) > 0, "No API operations found"
441
- strategies = [
442
- operation.as_strategy(
395
+ _strategies = [
396
+ operation.ok().as_strategy(
443
397
  hooks=hooks,
444
398
  auth_storage=auth_storage,
445
- data_generation_method=data_generation_method,
399
+ generation_mode=generation_mode,
446
400
  generation_config=generation_config,
447
401
  **kwargs,
448
402
  )
449
- for operations in self.operations.values()
450
- for operation in operations.values()
403
+ for operation in self.get_all_operations()
404
+ if isinstance(operation, Ok)
451
405
  ]
452
- return combine_strategies(strategies)
406
+ return strategies.combine(_strategies)
453
407
 
408
+ def configure(
409
+ self,
410
+ *,
411
+ base_url: str | None | NotSet = NOT_SET,
412
+ location: str | None | NotSet = NOT_SET,
413
+ rate_limit: str | None | NotSet = NOT_SET,
414
+ generation: GenerationConfig | NotSet = NOT_SET,
415
+ output: OutputConfig | NotSet = NOT_SET,
416
+ app: Any | NotSet = NOT_SET,
417
+ ) -> Self:
418
+ if not isinstance(base_url, NotSet):
419
+ self.base_url = base_url
420
+ if not isinstance(location, NotSet):
421
+ self.location = location
422
+ if not isinstance(rate_limit, NotSet):
423
+ if isinstance(rate_limit, str):
424
+ self.rate_limiter = build_limiter(rate_limit)
425
+ else:
426
+ self.rate_limiter = None
427
+ if not isinstance(generation, NotSet):
428
+ self.generation_config = generation
429
+ if not isinstance(output, NotSet):
430
+ self.output_config = output
431
+ if not isinstance(app, NotSet):
432
+ self.app = app
433
+ return self
454
434
 
455
- @dataclass
456
- class APIOperationMap(MutableMapping):
457
- data: MutableMapping
458
435
 
459
- def __setitem__(self, key: str, value: APIOperation) -> None:
460
- self.data[key] = value
436
+ @dataclass
437
+ class APIOperationMap(Mapping):
438
+ _schema: BaseSchema
439
+ _data: Mapping
461
440
 
462
441
  def __getitem__(self, item: str) -> APIOperation:
463
- return self.data[item]
464
-
465
- def __delitem__(self, key: str) -> None:
466
- del self.data[key]
442
+ return self._data[item]
467
443
 
468
444
  def __len__(self) -> int:
469
- return len(self.data)
445
+ return len(self._data)
470
446
 
471
447
  def __iter__(self) -> Iterator[str]:
472
- return iter(self.data)
448
+ return iter(self._data)
473
449
 
474
450
  def as_strategy(
475
451
  self,
476
452
  hooks: HookDispatcher | None = None,
477
453
  auth_storage: AuthStorage | None = None,
478
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
454
+ generation_mode: GenerationMode = GenerationMode.default(),
479
455
  generation_config: GenerationConfig | None = None,
480
456
  **kwargs: Any,
481
457
  ) -> SearchStrategy:
482
458
  """Build a strategy for generating test cases for all API operations defined in this subset."""
483
- assert len(self.data) > 0, "No API operations found"
484
- strategies = [
459
+ _strategies = [
485
460
  operation.as_strategy(
486
461
  hooks=hooks,
487
462
  auth_storage=auth_storage,
488
- data_generation_method=data_generation_method,
463
+ generation_mode=generation_mode,
489
464
  generation_config=generation_config,
490
465
  **kwargs,
491
466
  )
492
- for operation in self.data.values()
467
+ for operation in self._data.values()
493
468
  ]
494
- return combine_strategies(strategies)
469
+ return strategies.combine(_strategies)
470
+
471
+
472
+ @dataclass(eq=False)
473
+ class Parameter:
474
+ """A logically separate parameter bound to a location (e.g., to "query string").
475
+
476
+ For example, if the API requires multiple headers to be present, each header is presented as a separate
477
+ `Parameter` instance.
478
+ """
479
+
480
+ # The parameter definition in the language acceptable by the API
481
+ definition: Any
482
+
483
+ @property
484
+ def location(self) -> str:
485
+ """Where this parameter is located.
486
+
487
+ E.g. "query" or "body"
488
+ """
489
+ raise NotImplementedError
490
+
491
+ @property
492
+ def name(self) -> str:
493
+ """Parameter name."""
494
+ raise NotImplementedError
495
+
496
+ @property
497
+ def is_required(self) -> bool:
498
+ """Whether the parameter is required for a successful API call."""
499
+ raise NotImplementedError
500
+
501
+ def serialize(self, operation: APIOperation) -> str:
502
+ """Get parameter's string representation."""
503
+ raise NotImplementedError
504
+
505
+
506
+ P = TypeVar("P", bound=Parameter)
507
+
508
+
509
+ @dataclass
510
+ class ParameterSet(Generic[P]):
511
+ """A set of parameters for the same location."""
512
+
513
+ items: list[P] = field(default_factory=list)
514
+
515
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
516
+
517
+ def add(self, parameter: P) -> None:
518
+ """Add a new parameter."""
519
+ self.items.append(parameter)
520
+
521
+ def get(self, name: str) -> P | None:
522
+ for parameter in self:
523
+ if parameter.name == name:
524
+ return parameter
525
+ return None
526
+
527
+ def contains(self, name: str) -> bool:
528
+ return self.get(name) is not None
529
+
530
+ def __contains__(self, item: str) -> bool:
531
+ return self.contains(item)
532
+
533
+ def __bool__(self) -> bool:
534
+ return bool(self.items)
535
+
536
+ def __iter__(self) -> Generator[P, None, None]:
537
+ yield from iter(self.items)
538
+
539
+ def __len__(self) -> int:
540
+ return len(self.items)
541
+
542
+ def __getitem__(self, item: int) -> P:
543
+ return self.items[item]
544
+
545
+
546
+ class PayloadAlternatives(ParameterSet[P]):
547
+ """A set of alternative payloads."""
548
+
549
+
550
+ D = TypeVar("D", bound=dict)
551
+
552
+
553
+ @dataclass(repr=False)
554
+ class OperationDefinition(Generic[D]):
555
+ """A wrapper to store not resolved API operation definitions.
556
+
557
+ To prevent recursion errors we need to store definitions without resolving references. But operation definitions
558
+ itself can be behind a reference (when there is a ``$ref`` in ``paths`` values), therefore we need to store this
559
+ scope change to have a proper reference resolving later.
560
+ """
561
+
562
+ raw: D
563
+ resolved: D
564
+ scope: str
565
+
566
+ __slots__ = ("raw", "resolved", "scope")
567
+
568
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
569
+
570
+
571
+ @dataclass(eq=False)
572
+ class APIOperation(Generic[P]):
573
+ """A single operation defined in an API.
574
+
575
+ You can get one via a ``schema`` instance.
576
+
577
+ .. code-block:: python
578
+
579
+ # Get the POST /items operation
580
+ operation = schema["/items"]["POST"]
581
+
582
+ """
583
+
584
+ # `path` does not contain `basePath`
585
+ # Example <scheme>://<host>/<basePath>/users - "/users" is path
586
+ # https://swagger.io/docs/specification/2-0/api-host-and-base-path/
587
+ path: str
588
+ method: str
589
+ definition: OperationDefinition = field(repr=False)
590
+ schema: BaseSchema
591
+ label: str = None # type: ignore
592
+ app: Any = None
593
+ base_url: str | None = None
594
+ path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
595
+ headers: ParameterSet[P] = field(default_factory=ParameterSet)
596
+ cookies: ParameterSet[P] = field(default_factory=ParameterSet)
597
+ query: ParameterSet[P] = field(default_factory=ParameterSet)
598
+ body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
599
+
600
+ def __post_init__(self) -> None:
601
+ if self.label is None:
602
+ self.label = f"{self.method.upper()} {self.full_path}" # type: ignore
603
+
604
+ @property
605
+ def full_path(self) -> str:
606
+ return self.schema.get_full_path(self.path)
607
+
608
+ @property
609
+ def links(self) -> dict[str, dict[str, Any]]:
610
+ return self.schema.get_links(self)
611
+
612
+ @property
613
+ def tags(self) -> list[str] | None:
614
+ return self.schema.get_tags(self)
615
+
616
+ def iter_parameters(self) -> Iterator[P]:
617
+ """Iterate over all operation's parameters."""
618
+ return chain(self.path_parameters, self.headers, self.cookies, self.query)
619
+
620
+ def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
621
+ return {
622
+ "path": self.path_parameters,
623
+ "header": self.headers,
624
+ "cookie": self.cookies,
625
+ "query": self.query,
626
+ "body": self.body,
627
+ }.get(location)
628
+
629
+ def add_parameter(self, parameter: P) -> None:
630
+ """Add a new processed parameter to an API operation.
631
+
632
+ :param parameter: A parameter that will be used with this operation.
633
+ :rtype: None
634
+ """
635
+ # If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
636
+ # But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
637
+ # In this case, we still might generate some tests for an API operation, but without this parameter,
638
+ # which is better than skip the whole operation from testing.
639
+ container = self._lookup_container(parameter.location)
640
+ if container is not None:
641
+ container.add(parameter)
642
+
643
+ def get_parameter(self, name: str, location: str) -> P | None:
644
+ container = self._lookup_container(location)
645
+ if container is not None:
646
+ return container.get(name)
647
+ return None
648
+
649
+ def as_strategy(
650
+ self,
651
+ hooks: HookDispatcher | None = None,
652
+ auth_storage: AuthStorage | None = None,
653
+ generation_mode: GenerationMode = GenerationMode.default(),
654
+ generation_config: GenerationConfig | None = None,
655
+ **kwargs: Any,
656
+ ) -> SearchStrategy[Case]:
657
+ """Turn this API operation into a Hypothesis strategy."""
658
+ strategy = self.schema.get_case_strategy(
659
+ self, hooks, auth_storage, generation_mode, generation_config=generation_config, **kwargs
660
+ )
661
+
662
+ def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
663
+ context = HookContext(self)
664
+ for hook in dispatcher.get_all_by_name("before_generate_case"):
665
+ _strategy = hook(context, _strategy)
666
+ for hook in dispatcher.get_all_by_name("filter_case"):
667
+ hook = partial(hook, context)
668
+ _strategy = _strategy.filter(hook)
669
+ for hook in dispatcher.get_all_by_name("map_case"):
670
+ hook = partial(hook, context)
671
+ _strategy = _strategy.map(hook)
672
+ for hook in dispatcher.get_all_by_name("flatmap_case"):
673
+ hook = partial(hook, context)
674
+ _strategy = _strategy.flatmap(hook)
675
+ return _strategy
676
+
677
+ strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
678
+ strategy = _apply_hooks(self.schema.hooks, strategy)
679
+ if hooks is not None:
680
+ strategy = _apply_hooks(hooks, strategy)
681
+ return strategy
682
+
683
+ def get_security_requirements(self) -> list[str]:
684
+ return self.schema.get_security_requirements(self)
685
+
686
+ def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
687
+ """Get examples from the API operation."""
688
+ kwargs.setdefault("generation_config", self.schema.generation_config)
689
+ return self.schema.get_strategies_from_examples(self, **kwargs)
690
+
691
+ def get_parameter_serializer(self, location: str) -> Callable | None:
692
+ """Get a function that serializes parameters for the given location.
693
+
694
+ It handles serializing data into various `collectionFormat` options and similar.
695
+ Note that payload is handled by this function - it is handled by serializers.
696
+ """
697
+ return self.schema.get_parameter_serializer(self, location)
698
+
699
+ def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
700
+ return self.schema.prepare_multipart(form_data, self)
701
+
702
+ def get_request_payload_content_types(self) -> list[str]:
703
+ return self.schema.get_request_payload_content_types(self)
704
+
705
+ def _get_default_media_type(self) -> str:
706
+ # If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
707
+ media_types = self.get_request_payload_content_types()
708
+ if len(media_types) == 1:
709
+ # The only available option
710
+ return media_types[0]
711
+ media_types_repr = ", ".join(media_types)
712
+ raise IncorrectUsage(
713
+ "Can not detect appropriate media type. "
714
+ "You can either specify one of the defined media types "
715
+ f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
716
+ )
717
+
718
+ def Case(
719
+ self,
720
+ *,
721
+ method: str | None = None,
722
+ path_parameters: dict[str, Any] | None = None,
723
+ headers: dict[str, Any] | None = None,
724
+ cookies: dict[str, Any] | None = None,
725
+ query: dict[str, Any] | None = None,
726
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
727
+ media_type: str | None = None,
728
+ meta: CaseMetadata | None = None,
729
+ ) -> Case:
730
+ """Create a new example for this API operation.
731
+
732
+ The main use case is constructing Case instances completely manually, without data generation.
733
+ """
734
+ return self.schema.make_case(
735
+ operation=self,
736
+ method=method,
737
+ path_parameters=path_parameters,
738
+ headers=headers,
739
+ cookies=cookies,
740
+ query=query,
741
+ body=body,
742
+ media_type=media_type,
743
+ meta=meta,
744
+ )
745
+
746
+ @property
747
+ def operation_reference(self) -> str:
748
+ path = self.path.replace("~", "~0").replace("/", "~1")
749
+ return f"#/paths/{path}/{self.method}"
750
+
751
+ def validate_response(self, response: Response) -> bool | None:
752
+ """Validate API response for conformance.
753
+
754
+ :raises FailureGroup: If the response does not conform to the API schema.
755
+ """
756
+ return self.schema.validate_response(self, response)
757
+
758
+ def is_response_valid(self, response: Response) -> bool:
759
+ """Validate API response for conformance."""
760
+ try:
761
+ self.validate_response(response)
762
+ return True
763
+ except AssertionError:
764
+ return False
765
+
766
+ def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
767
+ return self.schema._get_payload_schema(self.definition.raw, media_type)
768
+
769
+ def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
770
+ return self.schema._get_payload_schema(self.definition.resolved, media_type)