schemathesis 3.39.16__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 +233 -307
  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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.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 -717
  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.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,247 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Iterator, cast
7
+
8
+ from schemathesis.core.failures import Failure
9
+ from schemathesis.core.transport import Response
10
+ from schemathesis.engine import Status
11
+ from schemathesis.generation.case import Case
12
+
13
+ if TYPE_CHECKING:
14
+ import requests
15
+
16
+ from schemathesis.generation.stateful.state_machine import Transition
17
+
18
+
19
+ @dataclass
20
+ class ScenarioRecorder:
21
+ """Tracks and organizes all data related to a logical block of testing.
22
+
23
+ Records test cases, their hierarchy, API interactions, and results of checks performed during execution.
24
+ """
25
+
26
+ # Human-readable label
27
+ label: str
28
+
29
+ # Recorded test cases
30
+ cases: dict[str, CaseNode]
31
+ # Results of checks categorized by test case ID
32
+ checks: dict[str, list[CheckNode]]
33
+ # Network interactions by test case ID
34
+ interactions: dict[str, Interaction]
35
+
36
+ __slots__ = ("label", "status", "roots", "cases", "checks", "interactions")
37
+
38
+ def __init__(self, *, label: str) -> None:
39
+ self.label = label
40
+ self.cases = {}
41
+ self.checks = {}
42
+ self.interactions = {}
43
+
44
+ def record_case(self, *, parent_id: str | None, transition: Transition | None, case: Case) -> None:
45
+ """Record a test case and its relationship to a parent, if applicable."""
46
+ self.cases[case.id] = CaseNode(value=case, parent_id=parent_id, transition=transition)
47
+
48
+ def record_response(self, *, case_id: str, response: Response) -> None:
49
+ """Record the API response for a given test case."""
50
+ request = Request.from_prepared_request(response.request)
51
+ self.interactions[case_id] = Interaction(request=request, response=response)
52
+
53
+ def record_request(self, *, case_id: str, request: requests.PreparedRequest) -> None:
54
+ """Record a network-level error for a given test case."""
55
+ self.interactions[case_id] = Interaction(request=Request.from_prepared_request(request), response=None)
56
+
57
+ def record_check_failure(self, *, name: str, case_id: str, code_sample: str, failure: Failure) -> None:
58
+ """Record a failure of a check for a given test case."""
59
+ self.checks.setdefault(case_id, []).append(
60
+ CheckNode(
61
+ name=name,
62
+ status=Status.FAILURE,
63
+ failure_info=CheckFailureInfo(code_sample=code_sample, failure=failure),
64
+ )
65
+ )
66
+
67
+ def record_check_success(self, *, name: str, case_id: str) -> None:
68
+ """Record a successful pass of a check for a given test case."""
69
+ self.checks.setdefault(case_id, []).append(CheckNode(name=name, status=Status.SUCCESS, failure_info=None))
70
+
71
+ def find_failure_data(self, *, parent_id: str, failure: Failure) -> FailureData:
72
+ """Retrieve the relevant test case & interaction data for a failure.
73
+
74
+ It may happen that a failure comes from a different test case if a check generated some additional
75
+ test cases & interactions.
76
+ """
77
+ case_id = failure.case_id or parent_id
78
+ case = self.cases[case_id].value
79
+ request = self.interactions[case_id].request
80
+ response = self.interactions[case_id].response
81
+ assert isinstance(response, Response)
82
+ headers = {key: value[0] for key, value in request.headers.items()}
83
+ return FailureData(case=case, headers=headers, verify=response.verify)
84
+
85
+ def find_parent(self, *, case_id: str) -> Case | None:
86
+ """Find the parent case of a given test case, if it exists."""
87
+ case = self.cases.get(case_id)
88
+ if case is not None and case.parent_id is not None:
89
+ parent = self.cases.get(case.parent_id)
90
+ # The recorder state should always be consistent
91
+ assert parent is not None, "Parent does not exist"
92
+ return parent.value
93
+ return None
94
+
95
+ def find_related(self, *, case_id: str) -> Iterator[Case]:
96
+ """Iterate over all cases in the tree, starting from the root."""
97
+ seen = {case_id}
98
+
99
+ # First, find the root by going up
100
+ current_id = case_id
101
+ while True:
102
+ current_node = self.cases.get(current_id)
103
+ if current_node is None or current_node.parent_id is None:
104
+ root_id = current_id
105
+ break
106
+ current_id = current_node.parent_id
107
+
108
+ # Then traverse the whole tree from root
109
+ def traverse(node_id: str) -> Iterator[Case]:
110
+ # Get all children
111
+ for case_id, node in self.cases.items():
112
+ if node.parent_id == node_id and case_id not in seen:
113
+ seen.add(case_id)
114
+ yield node.value
115
+ # Recurse into children
116
+ yield from traverse(case_id)
117
+
118
+ # Start traversal from root
119
+ root_node = self.cases.get(root_id)
120
+ if root_node and root_id not in seen:
121
+ seen.add(root_id)
122
+ yield root_node.value
123
+ yield from traverse(root_id)
124
+
125
+ def find_response(self, *, case_id: str) -> Response | None:
126
+ """Retrieve the API response for a given test case, if available."""
127
+ interaction = self.interactions.get(case_id)
128
+ if interaction is None or interaction.response is None:
129
+ return None
130
+ return interaction.response
131
+
132
+
133
+ @dataclass
134
+ class CaseNode:
135
+ """Represents a test case and its parent-child relationship."""
136
+
137
+ value: Case
138
+ parent_id: str | None
139
+ # Transition may be absent if `parent_id` is present for cases when a case is derived inside a check
140
+ # and outside of the implemented transition logic (e.g. Open API links)
141
+ transition: Transition | None
142
+
143
+ __slots__ = ("value", "parent_id", "transition")
144
+
145
+
146
+ @dataclass
147
+ class CheckNode:
148
+ name: str
149
+ status: Status
150
+ failure_info: CheckFailureInfo | None
151
+
152
+ __slots__ = ("name", "status", "failure_info")
153
+
154
+
155
+ @dataclass
156
+ class CheckFailureInfo:
157
+ code_sample: str
158
+ failure: Failure
159
+
160
+ __slots__ = ("code_sample", "failure")
161
+
162
+
163
+ def serialize_payload(payload: bytes) -> str:
164
+ return base64.b64encode(payload).decode()
165
+
166
+
167
+ @dataclass(repr=False)
168
+ class Request:
169
+ """Request data extracted from `Case`."""
170
+
171
+ method: str
172
+ uri: str
173
+ body: bytes | None
174
+ body_size: int | None
175
+ headers: dict[str, list[str]]
176
+
177
+ __slots__ = ("method", "uri", "body", "body_size", "headers", "_encoded_body_cache")
178
+
179
+ def __init__(
180
+ self,
181
+ method: str,
182
+ uri: str,
183
+ body: bytes | None,
184
+ body_size: int | None,
185
+ headers: dict[str, list[str]],
186
+ ):
187
+ self.method = method
188
+ self.uri = uri
189
+ self.body = body
190
+ self.body_size = body_size
191
+ self.headers = headers
192
+ self._encoded_body_cache: str | None = None
193
+
194
+ @classmethod
195
+ def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
196
+ """A prepared request version is already stored in `requests.Response`."""
197
+ body = prepared.body
198
+
199
+ if isinstance(body, str):
200
+ # can be a string for `application/x-www-form-urlencoded`
201
+ body = body.encode("utf-8")
202
+
203
+ # these values have `str` type at this point
204
+ uri = cast(str, prepared.url)
205
+ method = cast(str, prepared.method)
206
+ return cls(
207
+ uri=uri,
208
+ method=method,
209
+ headers={key: [value] for (key, value) in prepared.headers.items()},
210
+ body=body,
211
+ body_size=len(body) if body is not None else None,
212
+ )
213
+
214
+ @property
215
+ def encoded_body(self) -> str | None:
216
+ if self.body is not None:
217
+ if self._encoded_body_cache is None:
218
+ self._encoded_body_cache = serialize_payload(self.body)
219
+ return self._encoded_body_cache
220
+ return None
221
+
222
+
223
+ @dataclass
224
+ class Interaction:
225
+ """Represents a single interaction with the tested application."""
226
+
227
+ request: Request
228
+ response: Response | None
229
+ timestamp: float
230
+
231
+ __slots__ = ("request", "response", "timestamp")
232
+
233
+ def __init__(self, request: Request, response: Response | None) -> None:
234
+ self.request = request
235
+ self.response = response
236
+ self.timestamp = time.time()
237
+
238
+
239
+ @dataclass
240
+ class FailureData:
241
+ """Details about a test failure, including the case and its context."""
242
+
243
+ case: Case
244
+ headers: dict[str, str]
245
+ verify: bool
246
+
247
+ __slots__ = ("case", "headers", "verify")
schemathesis/errors.py ADDED
@@ -0,0 +1,43 @@
1
+ """Public Schemathesis errors."""
2
+
3
+ from schemathesis.core.errors import (
4
+ HookError,
5
+ IncorrectUsage,
6
+ InternalError,
7
+ InvalidHeadersExample,
8
+ InvalidRateLimit,
9
+ InvalidRegexPattern,
10
+ InvalidRegexType,
11
+ InvalidSchema,
12
+ InvalidStateMachine,
13
+ InvalidTransition,
14
+ LoaderError,
15
+ NoLinksFound,
16
+ OperationNotFound,
17
+ SchemathesisError,
18
+ SerializationError,
19
+ SerializationNotPossible,
20
+ TransitionValidationError,
21
+ UnboundPrefix,
22
+ )
23
+
24
+ __all__ = [
25
+ "HookError",
26
+ "IncorrectUsage",
27
+ "InternalError",
28
+ "InvalidHeadersExample",
29
+ "InvalidRateLimit",
30
+ "InvalidRegexPattern",
31
+ "InvalidRegexType",
32
+ "InvalidSchema",
33
+ "InvalidStateMachine",
34
+ "InvalidTransition",
35
+ "LoaderError",
36
+ "OperationNotFound",
37
+ "NoLinksFound",
38
+ "SchemathesisError",
39
+ "SerializationError",
40
+ "SerializationNotPossible",
41
+ "TransitionValidationError",
42
+ "UnboundPrefix",
43
+ ]
schemathesis/filters.py CHANGED
@@ -9,12 +9,11 @@ from functools import partial
9
9
  from types import SimpleNamespace
10
10
  from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
11
11
 
12
- from .exceptions import UsageError
13
- from .types import Filter as FilterType
14
- from .types import NotSet
12
+ from schemathesis.core.errors import IncorrectUsage
13
+ from schemathesis.core.transforms import resolve_pointer
15
14
 
16
15
  if TYPE_CHECKING:
17
- from .models import APIOperation
16
+ from schemathesis.schemas import APIOperation
18
17
 
19
18
 
20
19
  class HasAPIOperation(Protocol):
@@ -151,26 +150,8 @@ class FilterSet:
151
150
  def clone(self) -> FilterSet:
152
151
  return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
153
152
 
154
- def merge(self, other: FilterSet) -> FilterSet:
155
- def _merge(lhs: set[Filter], rhs: set[Filter]) -> set[Filter]:
156
- result = lhs.copy()
157
- for new in rhs:
158
- for old in lhs:
159
- for new_matcher in new.matchers:
160
- for old_matcher in old.matchers:
161
- if "=" in new_matcher.label and "=" in old_matcher.label:
162
- if new_matcher.label.split("=")[0] == old_matcher.label.split("=")[0]:
163
- result.remove(old)
164
- result.add(new)
165
- return result
166
-
167
- return FilterSet(
168
- _includes=_merge(self._includes, other._includes), _excludes=_merge(self._excludes, other._excludes)
169
- )
170
-
171
- def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
172
- """Get a filtered list of the given operations that match the filters."""
173
- return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
153
+ def applies_to(self, operation: APIOperation) -> bool:
154
+ return self.match(SimpleNamespace(operation=operation))
174
155
 
175
156
  def match(self, ctx: HasAPIOperation) -> bool:
176
157
  """Determines whether the given operation should be included based on the defined filters.
@@ -276,7 +257,7 @@ class FilterSet:
276
257
  if func is not None:
277
258
  matchers.append(Matcher.for_function(func))
278
259
  for attribute, expected, regex in (
279
- ("verbose_name", name, name_regex),
260
+ ("label", name, name_regex),
280
261
  ("method", method, method_regex),
281
262
  ("path", path, path_regex),
282
263
  ("tag", tag, tag_regex),
@@ -284,23 +265,31 @@ class FilterSet:
284
265
  ):
285
266
  if expected is not None and regex is not None:
286
267
  # To match anything the regex should match the expected value, hence passing them together is useless
287
- raise UsageError(ERROR_EXPECTED_AND_REGEX)
268
+ raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
288
269
  if expected is not None:
270
+ if attribute == "method":
271
+ expected = _normalize_method(expected)
289
272
  matchers.append(Matcher.for_value(attribute, expected))
290
273
  if regex is not None:
291
274
  matchers.append(Matcher.for_regex(attribute, regex))
292
275
 
293
276
  if not matchers:
294
- raise UsageError(ERROR_EMPTY_FILTER)
277
+ raise IncorrectUsage(ERROR_EMPTY_FILTER)
295
278
  filter_ = Filter(matchers=tuple(matchers))
296
279
  if filter_ in self._includes or filter_ in self._excludes:
297
- raise UsageError(ERROR_FILTER_EXISTS)
280
+ raise IncorrectUsage(ERROR_FILTER_EXISTS)
298
281
  if include:
299
282
  self._includes.add(filter_)
300
283
  else:
301
284
  self._excludes.add(filter_)
302
285
 
303
286
 
287
+ def _normalize_method(value: FilterValue) -> FilterValue:
288
+ if isinstance(value, list):
289
+ return [item.upper() for item in value]
290
+ return value.upper()
291
+
292
+
304
293
  def attach_filter_chain(
305
294
  target: Callable,
306
295
  attribute: str,
@@ -358,74 +347,6 @@ def is_deprecated(ctx: HasAPIOperation) -> bool:
358
347
  return ctx.operation.definition.raw.get("deprecated") is True
359
348
 
360
349
 
361
- def filter_set_from_components(
362
- *,
363
- include: bool,
364
- method: FilterType | None = None,
365
- endpoint: FilterType | None = None,
366
- tag: FilterType | None = None,
367
- operation_id: FilterType | None = None,
368
- skip_deprecated_operations: bool | None | NotSet = None,
369
- parent: FilterSet | None = None,
370
- ) -> FilterSet:
371
- def _is_defined(x: FilterType | None) -> bool:
372
- return x is not None and not isinstance(x, NotSet)
373
-
374
- def _prepare_filter(filter_: FilterType | None) -> RegexValue | None:
375
- if filter_ is None or isinstance(filter_, NotSet):
376
- return None
377
- if isinstance(filter_, str):
378
- return filter_
379
- return "|".join(f"({f})" for f in filter_)
380
-
381
- new = FilterSet()
382
-
383
- if _is_defined(method) or _is_defined(endpoint) or _is_defined(tag) or _is_defined(operation_id):
384
- new._add_filter(
385
- include,
386
- method_regex=_prepare_filter(method),
387
- path_regex=_prepare_filter(endpoint),
388
- tag_regex=_prepare_filter(tag),
389
- operation_id_regex=_prepare_filter(operation_id),
390
- )
391
- if skip_deprecated_operations is True and not any(
392
- matcher.label == is_deprecated.__name__ for exclude_ in new._excludes for matcher in exclude_.matchers
393
- ):
394
- new.exclude(func=is_deprecated)
395
- # Merge with the parent filter set
396
- if parent is not None:
397
- for include_ in parent._includes:
398
- matchers = include_.matchers
399
- ids = []
400
- for idx, matcher in enumerate(matchers):
401
- label = matcher.label
402
- if (
403
- (not isinstance(method, NotSet) and label.startswith("method_regex="))
404
- or (not isinstance(endpoint, NotSet) and label.startswith("path_regex="))
405
- or (not isinstance(tag, NotSet) and matcher.label.startswith("tag_regex="))
406
- or (not isinstance(operation_id, NotSet) and matcher.label.startswith("operation_id_regex="))
407
- ):
408
- ids.append(idx)
409
- if ids:
410
- matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
411
- if matchers:
412
- if new._includes:
413
- existing = new._includes.pop()
414
- matchers = existing.matchers + matchers
415
- new._includes.add(Filter(matchers=matchers))
416
- for exclude_ in parent._excludes:
417
- matchers = exclude_.matchers
418
- ids = []
419
- for idx, matcher in enumerate(exclude_.matchers):
420
- if skip_deprecated_operations is False and matcher.label == is_deprecated.__name__:
421
- ids.append(idx)
422
- if ids:
423
- matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
424
- if matchers:
425
- new._excludes.add(exclude_)
426
- return new
427
-
428
-
429
350
  def parse_expression(expression: str) -> tuple[str, str, Any]:
430
351
  expression = expression.strip()
431
352
 
@@ -452,8 +373,6 @@ def parse_expression(expression: str) -> tuple[str, str, Any]:
452
373
 
453
374
 
454
375
  def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
455
- from .specs.openapi.references import resolve_pointer
456
-
457
376
  pointer, op, value = parse_expression(expression)
458
377
 
459
378
  if op == "==":
@@ -1,17 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import random
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING
6
4
 
7
- from ._hypothesis import add_single_example, combine_strategies, get_single_example
8
- from ._methods import DataGenerationMethod, DataGenerationMethodInput
5
+ from schemathesis.generation.modes import GenerationMode
9
6
 
10
- if TYPE_CHECKING:
11
- from hypothesis.strategies import SearchStrategy
12
-
13
-
14
- DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
7
+ __all__ = [
8
+ "GenerationMode",
9
+ "generate_random_case_id",
10
+ ]
15
11
 
16
12
 
17
13
  CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@@ -27,27 +23,3 @@ def generate_random_case_id(length: int = 6) -> str:
27
23
  number, rem = divmod(number, BASE)
28
24
  output += CASE_ID_ALPHABET[rem]
29
25
  return output
30
-
31
-
32
- @dataclass
33
- class HeaderConfig:
34
- """Configuration for generating headers."""
35
-
36
- strategy: SearchStrategy[str] | None = None
37
-
38
-
39
- @dataclass
40
- class GenerationConfig:
41
- """Holds various configuration options relevant for data generation."""
42
-
43
- # Allow generating `\x00` bytes in strings
44
- allow_x00: bool = True
45
- # Allowing using `null` for optional arguments in GraphQL queries
46
- graphql_allow_null: bool = True
47
- # Generate strings using the given codec
48
- codec: str | None = "utf-8"
49
- # Whether to generate security parameters
50
- with_security_parameters: bool = True
51
- # Header generation configuration
52
- headers: HeaderConfig = field(default_factory=HeaderConfig)
53
- unexpected_methods: set[str] | None = None