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,278 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from functools import lru_cache
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ import hypothesis
9
+ from hypothesis.errors import InvalidDefinition
10
+ from hypothesis.stateful import RuleBasedStateMachine
11
+
12
+ from schemathesis.checks import CheckFunction
13
+ from schemathesis.core import DEFAULT_STATEFUL_STEP_COUNT
14
+ from schemathesis.core.errors import NoLinksFound
15
+ from schemathesis.core.result import Result
16
+ from schemathesis.core.transport import Response
17
+ from schemathesis.generation.case import Case
18
+
19
+ if TYPE_CHECKING:
20
+ import hypothesis
21
+ from requests.structures import CaseInsensitiveDict
22
+
23
+ from schemathesis.schemas import BaseSchema
24
+
25
+
26
+ DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
27
+ phases=[hypothesis.Phase.generate],
28
+ deadline=None,
29
+ stateful_step_count=DEFAULT_STATEFUL_STEP_COUNT,
30
+ suppress_health_check=list(hypothesis.HealthCheck),
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class StepInput:
36
+ """Input for a single state machine step."""
37
+
38
+ case: Case
39
+ transition: Transition | None # None for initial steps
40
+
41
+ __slots__ = ("case", "transition")
42
+
43
+ @classmethod
44
+ def initial(cls, case: Case) -> StepInput:
45
+ return cls(case=case, transition=None)
46
+
47
+
48
+ @dataclass
49
+ class Transition:
50
+ """Data about transition execution."""
51
+
52
+ # ID of the transition (e.g. link name)
53
+ id: str
54
+ parent_id: str
55
+ parameters: dict[str, dict[str, ExtractedParam]]
56
+ request_body: ExtractedParam | None
57
+
58
+ __slots__ = ("id", "parent_id", "parameters", "request_body")
59
+
60
+
61
+ @dataclass
62
+ class ExtractedParam:
63
+ """Result of parameter extraction."""
64
+
65
+ definition: Any
66
+ value: Result[Any, Exception]
67
+
68
+ __slots__ = ("definition", "value")
69
+
70
+
71
+ @dataclass
72
+ class ExtractionFailure:
73
+ """Represents a failure to extract data from a transition."""
74
+
75
+ # e.g., "GetUser"
76
+ id: str
77
+ case_id: str
78
+ # e.g., "POST /users"
79
+ source: str
80
+ # e.g., "GET /users/{userId}"
81
+ target: str
82
+ # e.g., "userId"
83
+ parameter_name: str
84
+ # e.g., "$response.body#/id"
85
+ expression: str
86
+ # Previous test cases in the chain, from newest to oldest
87
+ # Stored as a case + response pair
88
+ history: list[tuple[Case, Response]]
89
+ # The actual response that caused the failure
90
+ response: Response
91
+ error: Exception | None
92
+
93
+ __slots__ = ("id", "case_id", "source", "target", "parameter_name", "expression", "history", "response", "error")
94
+
95
+ def __eq__(self, other: object) -> bool:
96
+ assert isinstance(other, ExtractionFailure)
97
+ return (
98
+ self.source == other.source
99
+ and self.target == other.target
100
+ and self.id == other.id
101
+ and self.parameter_name == other.parameter_name
102
+ and self.expression == other.expression
103
+ )
104
+
105
+ def __hash__(self) -> int:
106
+ return hash(
107
+ (
108
+ self.source,
109
+ self.target,
110
+ self.id,
111
+ self.parameter_name,
112
+ self.expression,
113
+ )
114
+ )
115
+
116
+
117
+ @dataclass
118
+ class StepOutput:
119
+ """Output from a single transition of a state machine."""
120
+
121
+ response: Response
122
+ case: Case
123
+
124
+ __slots__ = ("response", "case")
125
+
126
+
127
+ def _normalize_name(name: str) -> str:
128
+ return re.sub(r"\W|^(?=\d)", "_", name).replace("__", "_")
129
+
130
+
131
+ class APIStateMachine(RuleBasedStateMachine):
132
+ """State machine for executing API operation sequences based on OpenAPI links.
133
+
134
+ Automatically generates test scenarios by chaining API operations according
135
+ to their defined relationships in the schema.
136
+ """
137
+
138
+ # This is a convenience attribute, which happened to clash with `RuleBasedStateMachine` instance level attribute
139
+ # They don't interfere, since it is properly overridden on the Hypothesis side, but it is likely that this
140
+ # attribute will be renamed in the future
141
+ bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
142
+ schema: BaseSchema
143
+
144
+ def __init__(self) -> None:
145
+ try:
146
+ super().__init__() # type: ignore
147
+ except InvalidDefinition as exc:
148
+ if "defines no rules" in str(exc):
149
+ if not self.schema.statistic.links.total:
150
+ message = "Schema contains no link definitions required for stateful testing"
151
+ else:
152
+ message = "All link definitions required for stateful testing are excluded by filters"
153
+ raise NoLinksFound(message) from None
154
+ raise
155
+ self.setup()
156
+
157
+ @classmethod
158
+ @lru_cache
159
+ def _to_test_case(cls) -> type:
160
+ from schemathesis.generation.stateful import run_state_machine_as_test
161
+
162
+ class StateMachineTestCase(RuleBasedStateMachine.TestCase):
163
+ settings = DEFAULT_STATE_MACHINE_SETTINGS
164
+
165
+ def runTest(self) -> None:
166
+ run_state_machine_as_test(cls, settings=self.settings)
167
+
168
+ runTest.is_hypothesis_test = True # type: ignore[attr-defined]
169
+
170
+ StateMachineTestCase.__name__ = cls.__name__ + ".TestCase"
171
+ StateMachineTestCase.__qualname__ = cls.__qualname__ + ".TestCase"
172
+ return StateMachineTestCase
173
+
174
+ def _new_name(self, target: str) -> str:
175
+ target = _normalize_name(target)
176
+ return super()._new_name(target) # type: ignore
177
+
178
+ def _get_target_for_result(self, result: StepOutput) -> str | None:
179
+ raise NotImplementedError
180
+
181
+ def _add_result_to_targets(self, targets: tuple[str, ...], result: StepOutput | None) -> None:
182
+ if result is None:
183
+ return
184
+ target = self._get_target_for_result(result)
185
+ if target is not None:
186
+ super()._add_result_to_targets((target,), result)
187
+
188
+ def _add_results_to_targets(self, targets: tuple[str, ...], results: list[StepOutput]) -> None:
189
+ # Hypothesis >6.131.15
190
+ for result in results:
191
+ target = self._get_target_for_result(result)
192
+ if target is not None:
193
+ super()._add_results_to_targets((target,), [result])
194
+
195
+ @classmethod
196
+ def run(cls, *, settings: hypothesis.settings | None = None) -> None:
197
+ """Execute the state machine test scenarios.
198
+
199
+ Args:
200
+ settings: Hypothesis settings for test execution.
201
+
202
+ """
203
+ from . import run_state_machine_as_test
204
+
205
+ __tracebackhide__ = True
206
+ return run_state_machine_as_test(cls, settings=settings)
207
+
208
+ def setup(self) -> None:
209
+ """Called once at the beginning of each test scenario."""
210
+
211
+ def teardown(self) -> None:
212
+ """Called once at the end of each test scenario."""
213
+
214
+ # To provide the return type in the rendered documentation
215
+ teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
216
+
217
+ def _step(self, input: StepInput) -> StepOutput | None:
218
+ __tracebackhide__ = True
219
+ return self.step(input)
220
+
221
+ def step(self, input: StepInput) -> StepOutput:
222
+ __tracebackhide__ = True
223
+ self.before_call(input.case)
224
+ kwargs = self.get_call_kwargs(input.case)
225
+ response = self.call(input.case, **kwargs)
226
+ self.after_call(response, input.case)
227
+ self.validate_response(response, input.case, **kwargs)
228
+ return StepOutput(response, input.case)
229
+
230
+ def before_call(self, case: Case) -> None:
231
+ """Called before each API operation in the scenario.
232
+
233
+ Args:
234
+ case: Test case data for the operation.
235
+
236
+ """
237
+
238
+ def after_call(self, response: Response, case: Case) -> None:
239
+ """Called after each API operation in the scenario.
240
+
241
+ Args:
242
+ response: HTTP response from the operation.
243
+ case: Test case data that was executed.
244
+
245
+ """
246
+
247
+ def call(self, case: Case, **kwargs: Any) -> Response:
248
+ return case.call(**kwargs)
249
+
250
+ def get_call_kwargs(self, case: Case) -> dict[str, Any]:
251
+ """Returns keyword arguments for the API call.
252
+
253
+ Args:
254
+ case: Test case being executed.
255
+
256
+ Returns:
257
+ Dictionary passed to the `case.call()` method.
258
+
259
+ """
260
+ return {}
261
+
262
+ def validate_response(
263
+ self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None, **kwargs: Any
264
+ ) -> None:
265
+ """Validates the API response using configured checks.
266
+
267
+ Args:
268
+ response: HTTP response to validate.
269
+ case: Test case that generated the response.
270
+ additional_checks: Extra validation functions to run.
271
+ kwargs: Transport-level keyword arguments.
272
+
273
+ Raises:
274
+ FailureGroup: When validation checks fail.
275
+
276
+ """
277
+ __tracebackhide__ = True
278
+ case.validate_response(response, additional_checks=additional_checks, transport_kwargs=kwargs)
@@ -0,0 +1,15 @@
1
+ from schemathesis.graphql.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
2
+
3
+ from ..specs.graphql import nodes
4
+ from ..specs.graphql.scalars import scalar
5
+
6
+ __all__ = [
7
+ "from_url",
8
+ "from_asgi",
9
+ "from_wsgi",
10
+ "from_file",
11
+ "from_path",
12
+ "from_dict",
13
+ "nodes",
14
+ "scalar",
15
+ ]
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from schemathesis.core.failures import Failure, Severity
6
+
7
+ if TYPE_CHECKING:
8
+ from graphql.error import GraphQLFormattedError
9
+
10
+
11
+ class UnexpectedGraphQLResponse(Failure):
12
+ """GraphQL response is not a JSON object."""
13
+
14
+ __slots__ = ("operation", "type_name", "title", "message", "case_id", "severity")
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ operation: str,
20
+ type_name: str,
21
+ title: str = "Unexpected GraphQL Response",
22
+ message: str,
23
+ case_id: str | None = None,
24
+ ) -> None:
25
+ self.operation = operation
26
+ self.type_name = type_name
27
+ self.title = title
28
+ self.message = message
29
+ self.case_id = case_id
30
+ self.severity = Severity.MEDIUM
31
+
32
+ @property
33
+ def _unique_key(self) -> str:
34
+ return self.type_name
35
+
36
+
37
+ class GraphQLClientError(Failure):
38
+ """GraphQL query has not been executed."""
39
+
40
+ __slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ operation: str,
46
+ message: str,
47
+ errors: list[GraphQLFormattedError],
48
+ title: str = "GraphQL client error",
49
+ case_id: str | None = None,
50
+ ) -> None:
51
+ self.operation = operation
52
+ self.errors = errors
53
+ self.title = title
54
+ self.message = message
55
+ self.case_id = case_id
56
+ self._unique_key_cache: str | None = None
57
+ self.severity = Severity.MEDIUM
58
+
59
+ @property
60
+ def _unique_key(self) -> str:
61
+ if self._unique_key_cache is None:
62
+ self._unique_key_cache = _group_graphql_errors(self.errors)
63
+ return self._unique_key_cache
64
+
65
+
66
+ class GraphQLServerError(Failure):
67
+ """GraphQL response indicates at least one server error."""
68
+
69
+ __slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ operation: str,
75
+ message: str,
76
+ errors: list[GraphQLFormattedError],
77
+ title: str = "GraphQL server error",
78
+ case_id: str | None = None,
79
+ ) -> None:
80
+ self.operation = operation
81
+ self.errors = errors
82
+ self.title = title
83
+ self.message = message
84
+ self.case_id = case_id
85
+ self._unique_key_cache: str | None = None
86
+ self.severity = Severity.CRITICAL
87
+
88
+ @property
89
+ def _unique_key(self) -> str:
90
+ if self._unique_key_cache is None:
91
+ self._unique_key_cache = _group_graphql_errors(self.errors)
92
+ return self._unique_key_cache
93
+
94
+
95
+ def _group_graphql_errors(errors: list[GraphQLFormattedError]) -> str:
96
+ entries = []
97
+ for error in errors:
98
+ message = error["message"]
99
+ if "locations" in error:
100
+ message += ";locations:"
101
+ for location in sorted(error["locations"]):
102
+ message += f"({location['line'], location['column']})"
103
+ if "path" in error:
104
+ message += ";path:"
105
+ for chunk in error["path"]:
106
+ message += str(chunk)
107
+ entries.append(message)
108
+ entries.sort()
109
+ return "".join(entries)
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from functools import lru_cache
5
+ from os import PathLike
6
+ from pathlib import Path
7
+ from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, TypeVar, cast
8
+
9
+ from schemathesis.config import SchemathesisConfig
10
+ from schemathesis.core.errors import LoaderError, LoaderErrorKind
11
+ from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
12
+ from schemathesis.hooks import HookContext, dispatch
13
+ from schemathesis.python import asgi, wsgi
14
+
15
+ if TYPE_CHECKING:
16
+ from graphql import DocumentNode
17
+
18
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
19
+
20
+
21
+ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
22
+ """Load GraphQL schema from an ASGI application via introspection.
23
+
24
+ Args:
25
+ path: Relative URL path to the GraphQL endpoint (e.g., "/graphql")
26
+ app: ASGI application instance
27
+ config: Custom configuration. If `None`, uses auto-discovered config
28
+ **kwargs: Additional request parameters passed to the ASGI test client.
29
+
30
+ Example:
31
+ ```python
32
+ from fastapi import FastAPI
33
+ import schemathesis
34
+
35
+ app = FastAPI()
36
+ schema = schemathesis.graphql.from_asgi("/graphql", app)
37
+ ```
38
+
39
+ """
40
+ require_relative_url(path)
41
+ kwargs.setdefault("json", {"query": get_introspection_query()})
42
+ client = asgi.get_client(app)
43
+ response = load_from_url(client.post, url=path, **kwargs)
44
+ schema = extract_schema_from_response(response, lambda r: r.json())
45
+ loaded = from_dict(schema=schema, config=config)
46
+ loaded.app = app
47
+ loaded.location = path
48
+ return loaded
49
+
50
+
51
+ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
52
+ """Load GraphQL schema from a WSGI application via introspection.
53
+
54
+ Args:
55
+ path: Relative URL path to the GraphQL endpoint (e.g., "/graphql")
56
+ app: WSGI application instance
57
+ config: Custom configuration. If `None`, uses auto-discovered config
58
+ **kwargs: Additional request parameters passed to the WSGI test client.
59
+
60
+ Example:
61
+ ```python
62
+ from flask import Flask
63
+ import schemathesis
64
+
65
+ app = Flask(__name__)
66
+ schema = schemathesis.graphql.from_wsgi("/graphql", app)
67
+ ```
68
+
69
+ """
70
+ require_relative_url(path)
71
+ prepare_request_kwargs(kwargs)
72
+ kwargs.setdefault("json", {"query": get_introspection_query()})
73
+ client = wsgi.get_client(app)
74
+ response = client.post(path=path, **kwargs)
75
+ raise_for_status(response)
76
+ schema = extract_schema_from_response(response, lambda r: r.json)
77
+ loaded = from_dict(schema=schema, config=config)
78
+ loaded.app = app
79
+ loaded.location = path
80
+ return loaded
81
+
82
+
83
+ def from_url(
84
+ url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
85
+ ) -> GraphQLSchema:
86
+ """Load GraphQL schema from a URL via introspection query.
87
+
88
+ Args:
89
+ url: Full URL to the GraphQL endpoint
90
+ config: Custom configuration. If `None`, uses auto-discovered config
91
+ wait_for_schema: Maximum time in seconds to wait for schema availability
92
+ **kwargs: Additional parameters passed to `requests.post()` (headers, timeout, auth, etc.).
93
+
94
+ Example:
95
+ ```python
96
+ import schemathesis
97
+
98
+ # Basic usage
99
+ schema = schemathesis.graphql.from_url("https://api.example.com/graphql")
100
+
101
+ # With authentication and timeout
102
+ schema = schemathesis.graphql.from_url(
103
+ "https://api.example.com/graphql",
104
+ headers={"Authorization": "Bearer token"},
105
+ timeout=30,
106
+ wait_for_schema=10.0
107
+ )
108
+ ```
109
+
110
+ """
111
+ import requests
112
+
113
+ kwargs.setdefault("json", {"query": get_introspection_query()})
114
+ response = load_from_url(requests.post, url=url, wait_for_schema=wait_for_schema, **kwargs)
115
+ schema = extract_schema_from_response(response, lambda r: r.json())
116
+ loaded = from_dict(schema, config=config)
117
+ loaded.location = url
118
+ return loaded
119
+
120
+
121
+ def from_path(
122
+ path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
123
+ ) -> GraphQLSchema:
124
+ """Load GraphQL schema from a filesystem path.
125
+
126
+ Args:
127
+ path: File path to the GraphQL schema file (.graphql, .gql)
128
+ config: Custom configuration. If `None`, uses auto-discovered config
129
+ encoding: Text encoding for reading the file
130
+
131
+ Example:
132
+ ```python
133
+ import schemathesis
134
+
135
+ # Load from GraphQL SDL file
136
+ schema = schemathesis.graphql.from_path("./schema.graphql")
137
+ ```
138
+
139
+ """
140
+ with open(path, encoding=encoding) as file:
141
+ loaded = from_file(file=file, config=config)
142
+ loaded.location = Path(path).absolute().as_uri()
143
+ return loaded
144
+
145
+
146
+ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
147
+ """Load GraphQL schema from a file-like object or string.
148
+
149
+ Args:
150
+ file: File-like object or raw string containing GraphQL SDL
151
+ config: Custom configuration. If `None`, uses auto-discovered config
152
+
153
+ Example:
154
+ ```python
155
+ import schemathesis
156
+
157
+ # From GraphQL SDL string
158
+ schema_sdl = '''
159
+ type Query {
160
+ user(id: ID!): User
161
+ }
162
+ type User {
163
+ id: ID!
164
+ name: String!
165
+ }
166
+ '''
167
+ schema = schemathesis.graphql.from_file(schema_sdl)
168
+
169
+ # From file object
170
+ with open("schema.graphql") as f:
171
+ schema = schemathesis.graphql.from_file(f)
172
+ ```
173
+
174
+ """
175
+ import graphql
176
+
177
+ if isinstance(file, str):
178
+ data = file
179
+ else:
180
+ data = file.read()
181
+ try:
182
+ document = graphql.build_schema(data)
183
+ result = graphql.execute(document, get_introspection_query_ast())
184
+ # TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
185
+ result = cast(graphql.ExecutionResult, result)
186
+ # TYPES:
187
+ # - `document` is a valid schema, because otherwise `build_schema` will rise an error;
188
+ # - `INTROSPECTION_QUERY` is a valid query - it is known upfront;
189
+ # Therefore the execution result is always valid at this point and `result.data` is not `None`
190
+ schema = cast(Dict[str, Any], result.data)
191
+ except Exception as exc:
192
+ try:
193
+ schema = json.loads(data)
194
+ if not isinstance(schema, dict) or "__schema" not in schema:
195
+ _on_invalid_schema(exc)
196
+ except json.JSONDecodeError:
197
+ _on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
198
+ return from_dict(schema, config=config)
199
+
200
+
201
+ def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
202
+ """Load GraphQL schema from a dictionary containing introspection result.
203
+
204
+ Args:
205
+ schema: Dictionary containing GraphQL introspection result or wrapped in 'data' key
206
+ config: Custom configuration. If `None`, uses auto-discovered config
207
+
208
+ Example:
209
+ ```python
210
+ import schemathesis
211
+
212
+ # From introspection result
213
+ introspection = {
214
+ "__schema": {
215
+ "types": [...],
216
+ "queryType": {"name": "Query"},
217
+ # ... rest of introspection result
218
+ }
219
+ }
220
+ schema = schemathesis.graphql.from_dict(introspection)
221
+
222
+ # From GraphQL response format (with 'data' wrapper)
223
+ response_data = {
224
+ "data": {
225
+ "__schema": {
226
+ "types": [...],
227
+ "queryType": {"name": "Query"}
228
+ }
229
+ }
230
+ }
231
+ schema = schemathesis.graphql.from_dict(response_data)
232
+ ```
233
+
234
+ """
235
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
236
+
237
+ if "data" in schema:
238
+ schema = schema["data"]
239
+ hook_context = HookContext()
240
+ dispatch("before_load_schema", hook_context, schema)
241
+
242
+ if config is None:
243
+ config = SchemathesisConfig.discover()
244
+ project_config = config.projects.get(schema)
245
+ instance = GraphQLSchema(schema, config=project_config)
246
+ dispatch("after_load_schema", hook_context, instance)
247
+ return instance
248
+
249
+
250
+ @lru_cache
251
+ def get_introspection_query() -> str:
252
+ import graphql
253
+
254
+ return graphql.get_introspection_query()
255
+
256
+
257
+ @lru_cache
258
+ def get_introspection_query_ast() -> DocumentNode:
259
+ import graphql
260
+
261
+ query = get_introspection_query()
262
+ return graphql.parse(query)
263
+
264
+
265
+ R = TypeVar("R")
266
+
267
+
268
+ def extract_schema_from_response(response: R, callback: Callable[[R], Any]) -> dict[str, Any]:
269
+ try:
270
+ decoded = callback(response)
271
+ except json.JSONDecodeError as exc:
272
+ raise LoaderError(
273
+ LoaderErrorKind.UNEXPECTED_CONTENT_TYPE,
274
+ "Received unsupported content while expecting a JSON payload for GraphQL",
275
+ ) from exc
276
+ return decoded
277
+
278
+
279
+ def _on_invalid_schema(exc: Exception, extras: list[str] | None = None) -> NoReturn:
280
+ raise LoaderError(
281
+ LoaderErrorKind.GRAPHQL_INVALID_SCHEMA,
282
+ "The provided API schema does not appear to be a valid GraphQL schema",
283
+ extras=extras or [],
284
+ ) from exc