schemathesis 3.13.0__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 (245) 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 -1016
  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 +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  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 +753 -74
  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 +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  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.13.0.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.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1 +0,0 @@
1
- from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from graphql import ValueNode
7
+
8
+ # Re-export `hypothesis_graphql` helpers
9
+
10
+ __all__ = [ # noqa: F822
11
+ "Boolean",
12
+ "Enum",
13
+ "Float",
14
+ "Int",
15
+ "List",
16
+ "Null",
17
+ "Object",
18
+ "String",
19
+ ]
20
+
21
+
22
+ def __getattr__(name: str) -> ValueNode | None:
23
+ if name in __all__:
24
+ import hypothesis_graphql.nodes
25
+
26
+ return getattr(hypothesis_graphql.nodes, name)
27
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import TYPE_CHECKING
5
+
6
+ from schemathesis.core.errors import IncorrectUsage
7
+
8
+ if TYPE_CHECKING:
9
+ import graphql
10
+ from hypothesis import strategies as st
11
+
12
+ CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
13
+
14
+
15
+ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
16
+ r"""Register a custom Hypothesis strategy for generating GraphQL scalar values.
17
+
18
+ Args:
19
+ name: Scalar name that matches your GraphQL schema scalar definition
20
+ strategy: Hypothesis strategy that generates GraphQL AST ValueNode objects
21
+
22
+ Example:
23
+ ```python
24
+ import schemathesis
25
+ from hypothesis import strategies as st
26
+ from schemathesis.graphql import nodes
27
+
28
+ # Register email scalar
29
+ schemathesis.graphql.scalar("Email", st.emails().map(nodes.String))
30
+
31
+ # Register positive integer scalar
32
+ schemathesis.graphql.scalar(
33
+ "PositiveInt",
34
+ st.integers(min_value=1).map(nodes.Int)
35
+ )
36
+
37
+ # Register phone number scalar
38
+ schemathesis.graphql.scalar(
39
+ "Phone",
40
+ st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}").map(nodes.String)
41
+ )
42
+ ```
43
+
44
+ Schema usage:
45
+ ```graphql
46
+ scalar Email
47
+ scalar PositiveInt
48
+
49
+ type Query {
50
+ getUser(email: Email!, rating: PositiveInt!): User
51
+ }
52
+ ```
53
+
54
+ """
55
+ from hypothesis.strategies import SearchStrategy
56
+
57
+ if not isinstance(name, str):
58
+ raise IncorrectUsage(f"Scalar name {name!r} must be a string")
59
+ if not isinstance(strategy, SearchStrategy):
60
+ raise IncorrectUsage(
61
+ f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
62
+ )
63
+ CUSTOM_SCALARS[name] = strategy
64
+
65
+
66
+ @lru_cache
67
+ def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
68
+ """Get all extra GraphQL strategies."""
69
+ from hypothesis import strategies as st
70
+
71
+ from . import nodes
72
+
73
+ dates = st.dates().map(str)
74
+ times = st.times().map("%sZ".__mod__)
75
+
76
+ return {
77
+ "Date": dates.map(nodes.String),
78
+ "Time": times.map(nodes.String),
79
+ "DateTime": st.tuples(dates, times).map("T".join).map(nodes.String),
80
+ "IP": st.ip_addresses().map(str).map(nodes.String),
81
+ "IPv4": st.ip_addresses(v=4).map(str).map(nodes.String),
82
+ "IPv6": st.ip_addresses(v=6).map(str).map(nodes.String),
83
+ "BigInt": st.integers().map(nodes.Int),
84
+ "Long": st.integers(min_value=-(2**63), max_value=2**63 - 1).map(nodes.Int),
85
+ "UUID": st.uuids().map(str).map(nodes.String),
86
+ }
@@ -1,166 +1,438 @@
1
- from functools import partial
2
- from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Type, TypeVar, Union, cast
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import time
5
+ from dataclasses import dataclass
6
+ from difflib import get_close_matches
7
+ from enum import unique
8
+ from types import SimpleNamespace
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Callable,
13
+ Generator,
14
+ Iterator,
15
+ Mapping,
16
+ NoReturn,
17
+ Union,
18
+ cast,
19
+ )
3
20
  from urllib.parse import urlsplit
4
21
 
5
- import attr
6
- import graphql
7
- import requests
8
22
  from hypothesis import strategies as st
9
- from hypothesis.strategies import SearchStrategy
10
- from hypothesis_graphql import strategies as gql_st
11
23
  from requests.structures import CaseInsensitiveDict
12
24
 
13
- from ...checks import not_a_server_error
14
- from ...constants import DataGenerationMethod
15
- from ...exceptions import InvalidSchema
16
- from ...hooks import HookDispatcher
17
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
18
- from ...schemas import BaseSchema
19
- from ...stateful import Stateful, StatefulTest
20
- from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
21
- from ...utils import NOT_SET, GenericResponse, Ok, Result
22
-
23
-
24
- @attr.s(slots=True, repr=False) # pragma: no mutate
25
- class GraphQLCase(Case):
26
- def as_requests_kwargs(
27
- self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
28
- ) -> Dict[str, Any]:
29
- final_headers = self._get_headers(headers)
30
- base_url = self._get_base_url(base_url)
31
- kwargs: Dict[str, Any] = {"method": self.method, "url": base_url, "headers": final_headers}
32
- # There is no direct way to have bytes here, but it is a useful pattern to support.
33
- # It also unifies GraphQLCase with its Open API counterpart where bytes may come from external examples
34
- if isinstance(self.body, bytes):
35
- kwargs["data"] = self.body
36
- # Assume that the payload is JSON, not raw GraphQL queries
37
- kwargs["headers"].setdefault("Content-Type", "application/json")
38
- else:
39
- kwargs["json"] = {"query": self.body}
40
- return kwargs
41
-
42
- def as_werkzeug_kwargs(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
43
- final_headers = self._get_headers(headers)
44
- return {
45
- "method": self.method,
46
- "path": self.operation.schema.get_full_path(self.formatted_path),
47
- # Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
48
- "headers": dict(final_headers),
49
- "query_string": self.query,
50
- "json": {"query": self.body},
51
- }
52
-
53
- def validate_response(
54
- self,
55
- response: GenericResponse,
56
- checks: Tuple[CheckFunction, ...] = (),
57
- additional_checks: Tuple[CheckFunction, ...] = (),
58
- code_sample_style: Optional[str] = None,
59
- ) -> None:
60
- checks = checks or (not_a_server_error,)
61
- checks += additional_checks
62
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
63
-
64
- def call_asgi(
65
- self,
66
- app: Any = None,
67
- base_url: Optional[str] = None,
68
- headers: Optional[Dict[str, str]] = None,
69
- **kwargs: Any,
70
- ) -> requests.Response:
71
- return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)
25
+ from schemathesis import auths
26
+ from schemathesis.core import NOT_SET, NotSet, Specification
27
+ from schemathesis.core.errors import InvalidSchema, OperationNotFound
28
+ from schemathesis.core.parameters import ParameterLocation
29
+ from schemathesis.core.result import Ok, Result
30
+ from schemathesis.generation import GenerationMode
31
+ from schemathesis.generation.case import Case
32
+ from schemathesis.generation.meta import (
33
+ CaseMetadata,
34
+ ComponentInfo,
35
+ ExamplesPhaseData,
36
+ FuzzingPhaseData,
37
+ GenerationInfo,
38
+ PhaseInfo,
39
+ TestPhase,
40
+ )
41
+ from schemathesis.hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
42
+ from schemathesis.schemas import (
43
+ APIOperation,
44
+ APIOperationMap,
45
+ ApiStatistic,
46
+ BaseSchema,
47
+ OperationDefinition,
48
+ )
49
+
50
+ from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
51
+
52
+ if TYPE_CHECKING:
53
+ import graphql
54
+ from hypothesis.strategies import SearchStrategy
55
+
56
+ from schemathesis.auths import AuthStorage
57
+
72
58
 
59
+ @unique
60
+ class RootType(enum.Enum):
61
+ QUERY = enum.auto()
62
+ MUTATION = enum.auto()
63
+
64
+
65
+ @dataclass(repr=False)
66
+ class GraphQLOperationDefinition(OperationDefinition):
67
+ field_name: str
68
+ type_: graphql.GraphQLType
69
+ root_type: RootType
70
+
71
+ __slots__ = ("raw", "field_name", "type_", "root_type")
72
+
73
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
74
+
75
+ @property
76
+ def is_query(self) -> bool:
77
+ return self.root_type == RootType.QUERY
78
+
79
+ @property
80
+ def is_mutation(self) -> bool:
81
+ return self.root_type == RootType.MUTATION
73
82
 
74
- C = TypeVar("C", bound=Case)
75
83
 
84
+ class GraphQLResponses:
85
+ def find_by_status_code(self, status_code: int) -> None:
86
+ return None # pragma: no cover
76
87
 
77
- @attr.s() # pragma: no mutate
88
+ def add(self, status_code: str, definition: dict[str, Any]) -> None:
89
+ return None # pragma: no cover
90
+
91
+
92
+ @dataclass
78
93
  class GraphQLSchema(BaseSchema):
94
+ def __repr__(self) -> str:
95
+ return f"<{self.__class__.__name__}>"
96
+
97
+ def __iter__(self) -> Iterator[str]:
98
+ schema = self.client_schema
99
+ for operation_type in (
100
+ schema.query_type,
101
+ schema.mutation_type,
102
+ ):
103
+ if operation_type is not None:
104
+ yield operation_type.name
105
+
106
+ def _get_operation_map(self, key: str) -> APIOperationMap:
107
+ schema = self.client_schema
108
+ for root_type, operation_type in (
109
+ (RootType.QUERY, schema.query_type),
110
+ (RootType.MUTATION, schema.mutation_type),
111
+ ):
112
+ if operation_type and operation_type.name == key:
113
+ map = APIOperationMap(self, {})
114
+ map._data = FieldMap(map, root_type, operation_type)
115
+ return map
116
+ raise KeyError(key)
117
+
118
+ def find_operation_by_label(self, label: str) -> APIOperation | None:
119
+ if label.startswith(("Query.", "Mutation.")):
120
+ ty, field = label.split(".", maxsplit=1)
121
+ try:
122
+ return self[ty][field]
123
+ except KeyError:
124
+ return None
125
+ return None
126
+
127
+ def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
128
+ raw_schema = self.raw_schema["__schema"]
129
+ type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
130
+ matches = get_close_matches(item, type_names)
131
+ message = f"`{item}` type not found"
132
+ if matches:
133
+ message += f". Did you mean `{matches[0]}`?"
134
+ raise OperationNotFound(message=message, item=item) from exc
135
+
79
136
  def get_full_path(self, path: str) -> str:
80
137
  return self.base_path
81
138
 
82
- @property # pragma: no mutate
83
- def verbose_name(self) -> str:
84
- return "GraphQL"
139
+ @property
140
+ def specification(self) -> Specification:
141
+ return Specification.graphql(version="")
85
142
 
86
143
  @property
87
144
  def client_schema(self) -> graphql.GraphQLSchema:
88
- return graphql.build_client_schema(self.raw_schema)
145
+ import graphql
146
+
147
+ if not hasattr(self, "_client_schema"):
148
+ self._client_schema = graphql.build_client_schema(self.raw_schema)
149
+ return self._client_schema
89
150
 
90
151
  @property
91
152
  def base_path(self) -> str:
92
- if self.base_url:
93
- return urlsplit(self.base_url).path
153
+ if self.config.base_url:
154
+ return urlsplit(self.config.base_url).path
94
155
  return self._get_base_path()
95
156
 
96
157
  def _get_base_path(self) -> str:
97
158
  return cast(str, urlsplit(self.location).path)
98
159
 
99
- @property
100
- def operations_count(self) -> int:
160
+ def _measure_statistic(self) -> ApiStatistic:
161
+ statistic = ApiStatistic()
101
162
  raw_schema = self.raw_schema["__schema"]
102
- if "queryType" not in raw_schema:
103
- return 0
104
- query_type_name = raw_schema["queryType"]["name"]
105
- for type_def in raw_schema.get("types", []):
106
- if type_def["name"] == query_type_name:
107
- return len(type_def["fields"])
108
- return 0
163
+ dummy_operation = APIOperation(
164
+ base_url=self.get_base_url(),
165
+ path=self.base_path,
166
+ label="",
167
+ method="POST",
168
+ schema=self,
169
+ responses=GraphQLResponses(),
170
+ security=None,
171
+ definition=None, # type: ignore[arg-type, var-annotated]
172
+ )
173
+
174
+ for type_name in ("queryType", "mutationType"):
175
+ type_def = raw_schema.get(type_name)
176
+ if type_def is not None:
177
+ query_type_name = type_def["name"]
178
+ for type_def in raw_schema.get("types", []):
179
+ if type_def["name"] == query_type_name:
180
+ for field in type_def["fields"]:
181
+ statistic.operations.total += 1
182
+ dummy_operation.label = f"{query_type_name}.{field['name']}"
183
+ if not self._should_skip(dummy_operation):
184
+ statistic.operations.selected += 1
185
+ return statistic
109
186
 
110
187
  def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
111
188
  schema = self.client_schema
112
- if schema.query_type is None:
113
- return
114
- for field_name, definition in schema.query_type.fields.items():
115
- yield Ok(
116
- APIOperation(
117
- base_url=self.get_base_url(),
118
- path=self.base_path,
119
- verbose_name=field_name,
120
- method="POST",
121
- app=self.app,
122
- schema=self,
123
- # Parameters are not yet supported
124
- definition=OperationDefinition(raw=definition, resolved=definition, scope="", parameters=[]),
125
- case_cls=GraphQLCase,
126
- )
127
- )
189
+ for root_type, operation_type in (
190
+ (RootType.QUERY, schema.query_type),
191
+ (RootType.MUTATION, schema.mutation_type),
192
+ ):
193
+ if operation_type is None:
194
+ continue
195
+ for field_name, field_ in operation_type.fields.items():
196
+ operation = self._build_operation(root_type, operation_type, field_name, field_)
197
+ if self._should_skip(operation):
198
+ continue
199
+ yield Ok(operation)
200
+
201
+ def _should_skip(
202
+ self,
203
+ operation: APIOperation,
204
+ _ctx_cache: SimpleNamespace = SimpleNamespace(operation=None),
205
+ ) -> bool:
206
+ _ctx_cache.operation = operation
207
+ return not self.filter_set.match(_ctx_cache)
208
+
209
+ def _build_operation(
210
+ self,
211
+ root_type: RootType,
212
+ operation_type: graphql.GraphQLObjectType,
213
+ field_name: str,
214
+ field: graphql.GraphQlField,
215
+ ) -> APIOperation:
216
+ return APIOperation(
217
+ base_url=self.get_base_url(),
218
+ path=self.base_path,
219
+ label=f"{operation_type.name}.{field_name}",
220
+ method="POST",
221
+ app=self.app,
222
+ schema=self,
223
+ responses=GraphQLResponses(),
224
+ security=None,
225
+ # Parameters are not yet supported
226
+ definition=GraphQLOperationDefinition(
227
+ raw=field,
228
+ type_=operation_type,
229
+ field_name=field_name,
230
+ root_type=root_type,
231
+ ),
232
+ )
128
233
 
129
234
  def get_case_strategy(
130
235
  self,
131
236
  operation: APIOperation,
132
- hooks: Optional[HookDispatcher] = None,
133
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
237
+ hooks: HookDispatcher | None = None,
238
+ auth_storage: AuthStorage | None = None,
239
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
240
+ **kwargs: Any,
134
241
  ) -> SearchStrategy:
135
- constructor = partial(GraphQLCase, operation=operation, data_generation_method=data_generation_method)
136
- return st.builds(constructor, body=gql_st.query(self.client_schema, fields=[operation.verbose_name]))
137
-
138
- def get_strategies_from_examples(self, operation: APIOperation) -> List[SearchStrategy[Case]]:
139
- return []
242
+ return graphql_cases(
243
+ operation=operation,
244
+ hooks=hooks,
245
+ auth_storage=auth_storage,
246
+ generation_mode=generation_mode,
247
+ **kwargs,
248
+ )
140
249
 
141
- def get_stateful_tests(
142
- self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
143
- ) -> Sequence[StatefulTest]:
250
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
144
251
  return []
145
252
 
146
253
  def make_case(
147
254
  self,
148
255
  *,
149
- case_cls: Type[C],
150
256
  operation: APIOperation,
151
- path_parameters: Optional[PathParameters] = None,
152
- headers: Optional[Headers] = None,
153
- cookies: Optional[Cookies] = None,
154
- query: Optional[Query] = None,
155
- body: Union[Body, NotSet] = NOT_SET,
156
- media_type: Optional[str] = None,
157
- ) -> C:
158
- return case_cls(
257
+ method: str | None = None,
258
+ path: str | None = None,
259
+ path_parameters: dict[str, Any] | None = None,
260
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
261
+ cookies: dict[str, Any] | None = None,
262
+ query: dict[str, Any] | None = None,
263
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
264
+ media_type: str | None = None,
265
+ meta: CaseMetadata | None = None,
266
+ ) -> Case:
267
+ return Case(
159
268
  operation=operation,
160
- path_parameters=path_parameters,
161
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
162
- cookies=cookies,
163
- query=query,
269
+ method=method or operation.method.upper(),
270
+ path=path or operation.path,
271
+ path_parameters=path_parameters or {},
272
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
273
+ cookies=cookies or {},
274
+ query=query or {},
164
275
  body=body,
165
- media_type=media_type,
276
+ media_type=media_type or "application/json",
277
+ meta=meta,
166
278
  )
279
+
280
+ def get_tags(self, operation: APIOperation) -> list[str] | None:
281
+ return None
282
+
283
+ def validate(self) -> None:
284
+ return None
285
+
286
+
287
+ @dataclass
288
+ class FieldMap(Mapping):
289
+ """Container for accessing API operations.
290
+
291
+ Provides a more specific error message if API operation is not found.
292
+ """
293
+
294
+ _parent: APIOperationMap
295
+ _root_type: RootType
296
+ _operation_type: graphql.GraphQLObjectType
297
+
298
+ __slots__ = ("_parent", "_root_type", "_operation_type")
299
+
300
+ def __len__(self) -> int:
301
+ return len(self._operation_type.fields)
302
+
303
+ def __iter__(self) -> Iterator[str]:
304
+ return iter(self._operation_type.fields)
305
+
306
+ def _init_operation(self, field_name: str) -> APIOperation:
307
+ schema = cast(GraphQLSchema, self._parent._schema)
308
+ operation_type = self._operation_type
309
+ field_ = operation_type.fields[field_name]
310
+ return schema._build_operation(self._root_type, operation_type, field_name, field_)
311
+
312
+ def __getitem__(self, item: str) -> APIOperation:
313
+ try:
314
+ return self._init_operation(item)
315
+ except KeyError as exc:
316
+ field_names = list(self._operation_type.fields)
317
+ matches = get_close_matches(item, field_names)
318
+ message = f"`{item}` field not found"
319
+ if matches:
320
+ message += f". Did you mean `{matches[0]}`?"
321
+ raise KeyError(message) from exc
322
+
323
+
324
+ @st.composite # type: ignore[misc]
325
+ def graphql_cases(
326
+ draw: Callable,
327
+ *,
328
+ operation: APIOperation,
329
+ hooks: HookDispatcher | None = None,
330
+ auth_storage: auths.AuthStorage | None = None,
331
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
332
+ path_parameters: NotSet | dict[str, Any] = NOT_SET,
333
+ headers: NotSet | dict[str, Any] = NOT_SET,
334
+ cookies: NotSet | dict[str, Any] = NOT_SET,
335
+ query: NotSet | dict[str, Any] = NOT_SET,
336
+ body: Any = NOT_SET,
337
+ media_type: str | None = None,
338
+ phase: TestPhase = TestPhase.FUZZING,
339
+ ) -> Any:
340
+ import graphql
341
+ from hypothesis_graphql import strategies as gql_st
342
+
343
+ start = time.monotonic()
344
+ definition = cast(GraphQLOperationDefinition, operation.definition)
345
+ strategy_factory = {
346
+ RootType.QUERY: gql_st.queries,
347
+ RootType.MUTATION: gql_st.mutations,
348
+ }[definition.root_type]
349
+ hook_context = HookContext(operation=operation)
350
+ custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
351
+ generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
352
+ strategy = strategy_factory(
353
+ operation.schema.client_schema, # type: ignore[attr-defined]
354
+ fields=[definition.field_name],
355
+ custom_scalars=custom_scalars,
356
+ print_ast=_noop,
357
+ allow_x00=generation.allow_x00,
358
+ allow_null=generation.graphql_allow_null,
359
+ codec=generation.codec,
360
+ )
361
+ strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
362
+ body = draw(strategy)
363
+
364
+ path_parameters_ = _generate_parameter(
365
+ ParameterLocation.PATH, path_parameters, draw, operation, hook_context, hooks
366
+ )
367
+ headers_ = _generate_parameter(ParameterLocation.HEADER, headers, draw, operation, hook_context, hooks)
368
+ cookies_ = _generate_parameter(ParameterLocation.COOKIE, cookies, draw, operation, hook_context, hooks)
369
+ query_ = _generate_parameter(ParameterLocation.QUERY, query, draw, operation, hook_context, hooks)
370
+
371
+ _phase_data = {
372
+ TestPhase.EXAMPLES: ExamplesPhaseData(
373
+ description="Positive test case",
374
+ parameter=None,
375
+ parameter_location=None,
376
+ location=None,
377
+ ),
378
+ TestPhase.FUZZING: FuzzingPhaseData(
379
+ description="Positive test case",
380
+ parameter=None,
381
+ parameter_location=None,
382
+ location=None,
383
+ ),
384
+ }[phase]
385
+ phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData], _phase_data)
386
+ instance = operation.Case(
387
+ path_parameters=path_parameters_,
388
+ headers=headers_,
389
+ cookies=cookies_,
390
+ query=query_,
391
+ body=body,
392
+ _meta=CaseMetadata(
393
+ generation=GenerationInfo(
394
+ time=time.monotonic() - start,
395
+ mode=generation_mode,
396
+ ),
397
+ phase=PhaseInfo(name=phase, data=phase_data),
398
+ components={
399
+ kind: ComponentInfo(mode=generation_mode)
400
+ for kind, value in [
401
+ (ParameterLocation.QUERY, query_),
402
+ (ParameterLocation.PATH, path_parameters_),
403
+ (ParameterLocation.HEADER, headers_),
404
+ (ParameterLocation.COOKIE, cookies_),
405
+ (ParameterLocation.BODY, body),
406
+ ]
407
+ if value is not NOT_SET
408
+ },
409
+ ),
410
+ media_type=media_type or "application/json",
411
+ )
412
+ context = auths.AuthContext(
413
+ operation=operation,
414
+ app=operation.app,
415
+ )
416
+ auths.set_on_case(instance, context, auth_storage)
417
+ return instance
418
+
419
+
420
+ def _generate_parameter(
421
+ location: ParameterLocation,
422
+ explicit: NotSet | dict[str, Any],
423
+ draw: Callable,
424
+ operation: APIOperation,
425
+ context: HookContext,
426
+ hooks: HookDispatcher | None,
427
+ ) -> Any:
428
+ # Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
429
+ container = location.container_name
430
+ if isinstance(explicit, NotSet):
431
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
432
+ else:
433
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
434
+ return draw(strategy)
435
+
436
+
437
+ def _noop(node: graphql.Node) -> graphql.Node:
438
+ return node