schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl

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