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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1 +0,0 @@
1
- from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
@@ -4,8 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
- from ...models import APIOperation
8
- from ...schemas import APIOperationMap
7
+ from ...schemas import APIOperation, APIOperationMap
9
8
 
10
9
 
11
10
  @dataclass
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from functools import lru_cache
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from ...exceptions import UsageError
6
+ from schemathesis.core.errors import IncorrectUsage
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  import graphql
@@ -13,17 +13,53 @@ CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
13
13
 
14
14
 
15
15
  def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
16
- """Register a new strategy for generating custom scalars.
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
+ ```
17
53
 
18
- :param str name: Scalar name. It should correspond the one used in the schema.
19
- :param strategy: Hypothesis strategy you'd like to use to generate values for this scalar.
20
54
  """
21
55
  from hypothesis.strategies import SearchStrategy
22
56
 
23
57
  if not isinstance(name, str):
24
- raise UsageError(f"Scalar name {name!r} must be a string")
58
+ raise IncorrectUsage(f"Scalar name {name!r} must be a string")
25
59
  if not isinstance(strategy, SearchStrategy):
26
- raise UsageError(f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar")
60
+ raise IncorrectUsage(
61
+ f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
62
+ )
27
63
  CUSTOM_SCALARS[name] = strategy
28
64
 
29
65
 
@@ -14,44 +14,49 @@ from typing import (
14
14
  Iterator,
15
15
  Mapping,
16
16
  NoReturn,
17
- Sequence,
18
- TypeVar,
17
+ Union,
19
18
  cast,
20
19
  )
21
- from urllib.parse import urlsplit, urlunsplit
20
+ from urllib.parse import urlsplit
22
21
 
23
22
  import graphql
24
23
  from hypothesis import strategies as st
25
24
  from hypothesis_graphql import strategies as gql_st
26
25
  from requests.structures import CaseInsensitiveDict
27
26
 
28
- from ... import auths
29
- from ...checks import not_a_server_error
30
- from ...constants import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER
31
- from ...exceptions import OperationNotFound, OperationSchemaError
32
- from ...generation import DataGenerationMethod, GenerationConfig
33
- from ...hooks import (
34
- GLOBAL_HOOK_DISPATCHER,
35
- HookContext,
36
- HookDispatcher,
37
- apply_to_all_dispatchers,
38
- should_skip_operation,
27
+ from schemathesis import auths
28
+ from schemathesis.core import NOT_SET, NotSet, Specification
29
+ from schemathesis.core.errors import InvalidSchema, OperationNotFound
30
+ from schemathesis.core.result import Ok, Result
31
+ from schemathesis.generation import GenerationMode
32
+ from schemathesis.generation.case import Case
33
+ from schemathesis.generation.meta import (
34
+ CaseMetadata,
35
+ ComponentInfo,
36
+ ComponentKind,
37
+ ExplicitPhaseData,
38
+ GeneratePhaseData,
39
+ GenerationInfo,
40
+ PhaseInfo,
41
+ TestPhase,
39
42
  )
40
- from ...internal.result import Ok, Result
41
- from ...models import APIOperation, Case, OperationDefinition
42
- from ...schemas import APIOperationMap, BaseSchema
43
- from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
44
- from ..openapi.constants import LOCATION_TO_CONTAINER
43
+ from schemathesis.hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
44
+ from schemathesis.schemas import (
45
+ APIOperation,
46
+ APIOperationMap,
47
+ ApiStatistic,
48
+ BaseSchema,
49
+ OperationDefinition,
50
+ )
51
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
52
+
45
53
  from ._cache import OperationCache
46
54
  from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
47
55
 
48
56
  if TYPE_CHECKING:
49
57
  from hypothesis.strategies import SearchStrategy
50
58
 
51
- from ...auths import AuthStorage
52
- from ...internal.checks import CheckFunction
53
- from ...stateful import Stateful, StatefulTest
54
- from ...transports.responses import GenericResponse
59
+ from schemathesis.auths import AuthStorage
55
60
 
56
61
 
57
62
  @unique
@@ -61,47 +66,13 @@ class RootType(enum.Enum):
61
66
 
62
67
 
63
68
  @dataclass(repr=False)
64
- class GraphQLCase(Case):
65
- def __hash__(self) -> int:
66
- return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
67
-
68
- def _get_url(self, base_url: str | None) -> str:
69
- base_url = self._get_base_url(base_url)
70
- # Replace the path, in case if the user provided any path parameters via hooks
71
- parts = list(urlsplit(base_url))
72
- parts[2] = self.formatted_path
73
- return urlunsplit(parts)
74
-
75
- def _get_body(self) -> Body | NotSet:
76
- return self.body if isinstance(self.body, (NotSet, bytes)) else {"query": self.body}
77
-
78
- def validate_response(
79
- self,
80
- response: GenericResponse,
81
- checks: tuple[CheckFunction, ...] = (),
82
- additional_checks: tuple[CheckFunction, ...] = (),
83
- excluded_checks: tuple[CheckFunction, ...] = (),
84
- code_sample_style: str | None = None,
85
- headers: dict[str, Any] | None = None,
86
- transport_kwargs: dict[str, Any] | None = None,
87
- ) -> None:
88
- checks = checks or (not_a_server_error,)
89
- checks += additional_checks
90
- checks = tuple(check for check in checks if check not in excluded_checks)
91
- return super().validate_response(
92
- response, checks, code_sample_style=code_sample_style, headers=headers, transport_kwargs=transport_kwargs
93
- )
94
-
95
-
96
- C = TypeVar("C", bound=Case)
97
-
98
-
99
- @dataclass
100
69
  class GraphQLOperationDefinition(OperationDefinition):
101
70
  field_name: str
102
71
  type_: graphql.GraphQLType
103
72
  root_type: RootType
104
73
 
74
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
75
+
105
76
  @property
106
77
  def is_query(self) -> bool:
107
78
  return self.root_type == RootType.QUERY
@@ -144,6 +115,15 @@ class GraphQLSchema(BaseSchema):
144
115
  return map
145
116
  raise KeyError(key)
146
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
+
147
127
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
148
128
  raw_schema = self.raw_schema["__schema"]
149
129
  type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
@@ -157,8 +137,8 @@ class GraphQLSchema(BaseSchema):
157
137
  return self.base_path
158
138
 
159
139
  @property
160
- def verbose_name(self) -> str:
161
- return "GraphQL"
140
+ def specification(self) -> Specification:
141
+ return Specification.graphql(version="")
162
142
 
163
143
  @property
164
144
  def client_schema(self) -> graphql.GraphQLSchema:
@@ -168,34 +148,39 @@ class GraphQLSchema(BaseSchema):
168
148
 
169
149
  @property
170
150
  def base_path(self) -> str:
171
- if self.base_url:
172
- return urlsplit(self.base_url).path
151
+ if self.config.base_url:
152
+ return urlsplit(self.config.base_url).path
173
153
  return self._get_base_path()
174
154
 
175
155
  def _get_base_path(self) -> str:
176
156
  return cast(str, urlsplit(self.location).path)
177
157
 
178
- @property
179
- def operations_count(self) -> int:
158
+ def _measure_statistic(self) -> ApiStatistic:
159
+ statistic = ApiStatistic()
180
160
  raw_schema = self.raw_schema["__schema"]
181
- total = 0
161
+ dummy_operation = APIOperation(
162
+ base_url=self.get_base_url(),
163
+ path=self.base_path,
164
+ label="",
165
+ method="POST",
166
+ schema=self,
167
+ definition=None, # type: ignore
168
+ )
169
+
182
170
  for type_name in ("queryType", "mutationType"):
183
171
  type_def = raw_schema.get(type_name)
184
172
  if type_def is not None:
185
173
  query_type_name = type_def["name"]
186
174
  for type_def in raw_schema.get("types", []):
187
175
  if type_def["name"] == query_type_name:
188
- total += len(type_def["fields"])
189
- return total
190
-
191
- @property
192
- def links_count(self) -> int:
193
- # Links are not supported for GraphQL
194
- return 0
195
-
196
- def get_all_operations(
197
- self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
198
- ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
176
+ for field in type_def["fields"]:
177
+ statistic.operations.total += 1
178
+ dummy_operation.label = f"{query_type_name}.{field['name']}"
179
+ if not self._should_skip(dummy_operation):
180
+ statistic.operations.selected += 1
181
+ return statistic
182
+
183
+ def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
199
184
  schema = self.client_schema
200
185
  for root_type, operation_type in (
201
186
  (RootType.QUERY, schema.query_type),
@@ -207,13 +192,6 @@ class GraphQLSchema(BaseSchema):
207
192
  operation = self._build_operation(root_type, operation_type, field_name, field_)
208
193
  if self._should_skip(operation):
209
194
  continue
210
- context = HookContext(operation=operation)
211
- if (
212
- should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
213
- or should_skip_operation(self.hooks, context)
214
- or (hooks and should_skip_operation(hooks, context))
215
- ):
216
- continue
217
195
  yield Ok(operation)
218
196
 
219
197
  def _should_skip(
@@ -234,7 +212,7 @@ class GraphQLSchema(BaseSchema):
234
212
  return APIOperation(
235
213
  base_url=self.get_base_url(),
236
214
  path=self.base_path,
237
- verbose_name=f"{operation_type.name}.{field_name}",
215
+ label=f"{operation_type.name}.{field_name}",
238
216
  method="POST",
239
217
  app=self.app,
240
218
  schema=self,
@@ -247,7 +225,6 @@ class GraphQLSchema(BaseSchema):
247
225
  field_name=field_name,
248
226
  root_type=root_type,
249
227
  ),
250
- case_cls=GraphQLCase,
251
228
  )
252
229
 
253
230
  def get_case_strategy(
@@ -255,51 +232,45 @@ class GraphQLSchema(BaseSchema):
255
232
  operation: APIOperation,
256
233
  hooks: HookDispatcher | None = None,
257
234
  auth_storage: AuthStorage | None = None,
258
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
259
- generation_config: GenerationConfig | None = None,
235
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
260
236
  **kwargs: Any,
261
237
  ) -> SearchStrategy:
262
- return get_case_strategy(
238
+ return graphql_cases(
263
239
  operation=operation,
264
- client_schema=self.client_schema,
265
240
  hooks=hooks,
266
241
  auth_storage=auth_storage,
267
- data_generation_method=data_generation_method,
268
- generation_config=generation_config or self.generation_config,
242
+ generation_mode=generation_mode,
269
243
  **kwargs,
270
244
  )
271
245
 
272
- def get_strategies_from_examples(
273
- self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
274
- ) -> list[SearchStrategy[Case]]:
275
- return []
276
-
277
- def get_stateful_tests(
278
- self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
279
- ) -> Sequence[StatefulTest]:
246
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
280
247
  return []
281
248
 
282
249
  def make_case(
283
250
  self,
284
251
  *,
285
- case_cls: type[C],
286
252
  operation: APIOperation,
287
- path_parameters: PathParameters | None = None,
288
- headers: Headers | None = None,
289
- cookies: Cookies | None = None,
290
- query: Query | None = None,
291
- body: Body | NotSet = NOT_SET,
253
+ method: str | None = None,
254
+ path: str | None = None,
255
+ path_parameters: dict[str, Any] | None = None,
256
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
257
+ cookies: dict[str, Any] | None = None,
258
+ query: dict[str, Any] | None = None,
259
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
292
260
  media_type: str | None = None,
293
- ) -> C:
294
- return case_cls(
261
+ meta: CaseMetadata | None = None,
262
+ ) -> Case:
263
+ return Case(
295
264
  operation=operation,
296
- path_parameters=path_parameters,
297
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
298
- cookies=cookies,
299
- query=query,
265
+ method=method or operation.method.upper(),
266
+ path=path or operation.path,
267
+ path_parameters=path_parameters or {},
268
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
269
+ cookies=cookies or {},
270
+ query=query or {},
300
271
  body=body,
301
272
  media_type=media_type or "application/json",
302
- generation_time=0.0,
273
+ meta=meta,
303
274
  )
304
275
 
305
276
  def get_tags(self, operation: APIOperation) -> list[str] | None:
@@ -353,15 +324,20 @@ class FieldMap(Mapping):
353
324
 
354
325
 
355
326
  @st.composite # type: ignore
356
- def get_case_strategy(
327
+ def graphql_cases(
357
328
  draw: Callable,
329
+ *,
358
330
  operation: APIOperation,
359
- client_schema: graphql.GraphQLSchema,
360
331
  hooks: HookDispatcher | None = None,
361
- auth_storage: AuthStorage | None = None,
362
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
363
- generation_config: GenerationConfig | None = None,
364
- **kwargs: Any,
332
+ auth_storage: auths.AuthStorage | None = None,
333
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
334
+ path_parameters: NotSet | dict[str, Any] = NOT_SET,
335
+ headers: NotSet | dict[str, Any] = NOT_SET,
336
+ cookies: NotSet | dict[str, Any] = NOT_SET,
337
+ query: NotSet | dict[str, Any] = NOT_SET,
338
+ body: Any = NOT_SET,
339
+ media_type: str | None = None,
340
+ phase: TestPhase = TestPhase.FUZZING,
365
341
  ) -> Any:
366
342
  start = time.monotonic()
367
343
  definition = cast(GraphQLOperationDefinition, operation.definition)
@@ -369,36 +345,56 @@ def get_case_strategy(
369
345
  RootType.QUERY: gql_st.queries,
370
346
  RootType.MUTATION: gql_st.mutations,
371
347
  }[definition.root_type]
372
- hook_context = HookContext(operation)
373
- generation_config = generation_config or GenerationConfig()
348
+ hook_context = HookContext(operation=operation)
374
349
  custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
350
+ generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
375
351
  strategy = strategy_factory(
376
- client_schema,
352
+ operation.schema.client_schema, # type: ignore[attr-defined]
377
353
  fields=[definition.field_name],
378
354
  custom_scalars=custom_scalars,
379
355
  print_ast=_noop, # type: ignore
380
- allow_x00=generation_config.allow_x00,
381
- allow_null=generation_config.graphql_allow_null,
382
- codec=generation_config.codec,
356
+ allow_x00=generation.allow_x00,
357
+ allow_null=generation.graphql_allow_null,
358
+ codec=generation.codec,
383
359
  )
384
360
  strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
385
361
  body = draw(strategy)
386
362
 
387
- path_parameters_ = _generate_parameter("path", draw, operation, hook_context, hooks)
388
- headers_ = _generate_parameter("header", draw, operation, hook_context, hooks)
389
- cookies_ = _generate_parameter("cookie", draw, operation, hook_context, hooks)
390
- query_ = _generate_parameter("query", draw, operation, hook_context, hooks)
391
-
392
- instance = GraphQLCase(
363
+ path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
364
+ headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
365
+ cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
366
+ query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
367
+
368
+ _phase_data = {
369
+ TestPhase.EXAMPLES: ExplicitPhaseData(),
370
+ TestPhase.FUZZING: GeneratePhaseData(),
371
+ }[phase]
372
+ phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
373
+ instance = operation.Case(
393
374
  path_parameters=path_parameters_,
394
375
  headers=headers_,
395
376
  cookies=cookies_,
396
377
  query=query_,
397
378
  body=body,
398
- operation=operation,
399
- data_generation_method=data_generation_method,
400
- generation_time=time.monotonic() - start,
401
- media_type="application/json",
379
+ _meta=CaseMetadata(
380
+ generation=GenerationInfo(
381
+ time=time.monotonic() - start,
382
+ mode=generation_mode,
383
+ ),
384
+ phase=PhaseInfo(name=phase, data=phase_data),
385
+ components={
386
+ kind: ComponentInfo(mode=generation_mode)
387
+ for kind, value in [
388
+ (ComponentKind.QUERY, query_),
389
+ (ComponentKind.PATH_PARAMETERS, path_parameters_),
390
+ (ComponentKind.HEADERS, headers_),
391
+ (ComponentKind.COOKIES, cookies_),
392
+ (ComponentKind.BODY, body),
393
+ ]
394
+ if value is not NOT_SET
395
+ },
396
+ ),
397
+ media_type=media_type or "application/json",
402
398
  ) # type: ignore
403
399
  context = auths.AuthContext(
404
400
  operation=operation,
@@ -409,11 +405,19 @@ def get_case_strategy(
409
405
 
410
406
 
411
407
  def _generate_parameter(
412
- location: str, draw: Callable, operation: APIOperation, context: HookContext, hooks: HookDispatcher | None
408
+ location: str,
409
+ explicit: NotSet | dict[str, Any],
410
+ draw: Callable,
411
+ operation: APIOperation,
412
+ context: HookContext,
413
+ hooks: HookDispatcher | None,
413
414
  ) -> Any:
414
415
  # Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
415
416
  container = LOCATION_TO_CONTAINER[location]
416
- strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
417
+ if isinstance(explicit, NotSet):
418
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
419
+ else:
420
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
417
421
  return draw(strategy)
418
422
 
419
423
 
@@ -1,10 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any, List, cast
2
4
 
3
- from ... import failures
4
- from ...exceptions import get_grouped_graphql_error, get_unexpected_graphql_response_error
5
+ from schemathesis.generation.case import Case
6
+ from schemathesis.graphql.checks import GraphQLClientError, GraphQLServerError, UnexpectedGraphQLResponse
5
7
 
6
8
 
7
- def validate_graphql_response(payload: Any) -> None:
9
+ def validate_graphql_response(case: Case, payload: Any) -> None:
8
10
  """Validate GraphQL response.
9
11
 
10
12
  Semantically valid GraphQL responses are JSON objects and may contain `data` or `errors` keys.
@@ -12,28 +14,20 @@ def validate_graphql_response(payload: Any) -> None:
12
14
  from graphql.error import GraphQLFormattedError
13
15
 
14
16
  if not isinstance(payload, dict):
15
- exc_class = get_unexpected_graphql_response_error(type(payload))
16
- raise exc_class(
17
- failures.UnexpectedGraphQLResponse.title,
18
- context=failures.UnexpectedGraphQLResponse(message="GraphQL response is not a JSON object"),
17
+ raise UnexpectedGraphQLResponse(
18
+ operation=case.operation.label,
19
+ message="GraphQL response is not a JSON object",
20
+ type_name=str(type(payload)),
19
21
  )
20
22
 
21
23
  errors = cast(List[GraphQLFormattedError], payload.get("errors"))
22
24
  if errors is not None and len(errors) > 0:
23
- exc_class = get_grouped_graphql_error(errors)
24
25
  data = payload.get("data")
25
26
  # There is no `path` pointing to some part of the input query, assuming client error
26
27
  if data is None and "path" not in errors[0]:
27
- message = errors[0]["message"]
28
- raise exc_class(
29
- failures.GraphQLClientError.title,
30
- context=failures.GraphQLClientError(message=message, errors=errors),
31
- )
28
+ raise GraphQLClientError(operation=case.operation.label, message=errors[0]["message"], errors=errors)
32
29
  if len(errors) > 1:
33
30
  message = "\n\n".join([f"{idx}. {error['message']}" for idx, error in enumerate(errors, 1)])
34
31
  else:
35
32
  message = errors[0]["message"]
36
- raise exc_class(
37
- failures.GraphQLServerError.title,
38
- context=failures.GraphQLServerError(message=message, errors=errors),
39
- )
33
+ raise GraphQLServerError(operation=case.operation.label, message=message, errors=errors)
@@ -1,4 +1,9 @@
1
1
  from .formats import register_string_format as format
2
2
  from .formats import unregister_string_format
3
- from .loaders import from_aiohttp, from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
4
3
  from .media_types import register_media_type as media_type
4
+
5
+ __all__ = [
6
+ "format",
7
+ "unregister_string_format",
8
+ "media_type",
9
+ ]
@@ -4,8 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING, Any, Tuple
5
5
 
6
6
  if TYPE_CHECKING:
7
- from ...models import APIOperation
8
- from ...schemas import APIOperationMap
7
+ from ...schemas import APIOperation, APIOperationMap
9
8
 
10
9
 
11
10
  @dataclass