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

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