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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,219 +1,438 @@
1
- # Pylint bug - `Callable` is used below
2
- # pylint: disable=unused-import
3
- from typing import Any, Callable, 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
+ )
4
20
  from urllib.parse import urlsplit
5
21
 
6
- import attr
7
- import graphql
8
- import requests
9
22
  from hypothesis import strategies as st
10
- from hypothesis.strategies import SearchStrategy
11
- from hypothesis_graphql import strategies as gql_st
12
23
  from requests.structures import CaseInsensitiveDict
13
24
 
14
- from ... import auth
15
- from ...auth import AuthStorage
16
- from ...checks import not_a_server_error
17
- from ...constants import DataGenerationMethod
18
- from ...exceptions import InvalidSchema
19
- from ...hooks import HookDispatcher
20
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
21
- from ...schemas import BaseSchema
22
- from ...stateful import Stateful, StatefulTest
23
- from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
24
- from ...utils import NOT_SET, GenericResponse, Ok, Result
25
- from .scalars import CUSTOM_SCALARS
26
-
27
-
28
- @attr.s(slots=True, repr=False) # pragma: no mutate
29
- class GraphQLCase(Case):
30
- def as_requests_kwargs(
31
- self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
32
- ) -> Dict[str, Any]:
33
- final_headers = self._get_headers(headers)
34
- base_url = self._get_base_url(base_url)
35
- kwargs: Dict[str, Any] = {"method": self.method, "url": base_url, "headers": final_headers}
36
- # There is no direct way to have bytes here, but it is a useful pattern to support.
37
- # It also unifies GraphQLCase with its Open API counterpart where bytes may come from external examples
38
- if isinstance(self.body, bytes):
39
- kwargs["data"] = self.body
40
- # Assume that the payload is JSON, not raw GraphQL queries
41
- kwargs["headers"].setdefault("Content-Type", "application/json")
42
- else:
43
- kwargs["json"] = {"query": self.body}
44
- return kwargs
45
-
46
- def as_werkzeug_kwargs(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
47
- final_headers = self._get_headers(headers)
48
- return {
49
- "method": self.method,
50
- "path": self.operation.schema.get_full_path(self.formatted_path),
51
- # Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
52
- "headers": dict(final_headers),
53
- "query_string": self.query,
54
- "json": {"query": self.body},
55
- }
56
-
57
- def validate_response(
58
- self,
59
- response: GenericResponse,
60
- checks: Tuple[CheckFunction, ...] = (),
61
- additional_checks: Tuple[CheckFunction, ...] = (),
62
- code_sample_style: Optional[str] = None,
63
- ) -> None:
64
- checks = checks or (not_a_server_error,)
65
- checks += additional_checks
66
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
67
-
68
- def call_asgi(
69
- self,
70
- app: Any = None,
71
- base_url: Optional[str] = None,
72
- headers: Optional[Dict[str, str]] = None,
73
- **kwargs: Any,
74
- ) -> requests.Response:
75
- 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
+ )
76
49
 
50
+ from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
77
51
 
78
- C = TypeVar("C", bound=Case)
52
+ if TYPE_CHECKING:
53
+ import graphql
54
+ from hypothesis.strategies import SearchStrategy
79
55
 
56
+ from schemathesis.auths import AuthStorage
80
57
 
81
- @attr.s()
58
+
59
+ @unique
60
+ class RootType(enum.Enum):
61
+ QUERY = enum.auto()
62
+ MUTATION = enum.auto()
63
+
64
+
65
+ @dataclass(repr=False)
82
66
  class GraphQLOperationDefinition(OperationDefinition):
83
- field_name: str = attr.ib()
84
- type_: graphql.GraphQLType = attr.ib()
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
82
+
83
+
84
+ class GraphQLResponses:
85
+ def find_by_status_code(self, status_code: int) -> None:
86
+ return None # pragma: no cover
87
+
88
+ def add(self, status_code: str, definition: dict[str, Any]) -> None:
89
+ return None # pragma: no cover
85
90
 
86
91
 
87
- @attr.s() # pragma: no mutate
92
+ @dataclass
88
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
+
89
136
  def get_full_path(self, path: str) -> str:
90
137
  return self.base_path
91
138
 
92
- @property # pragma: no mutate
93
- def verbose_name(self) -> str:
94
- return "GraphQL"
139
+ @property
140
+ def specification(self) -> Specification:
141
+ return Specification.graphql(version="")
95
142
 
96
143
  @property
97
144
  def client_schema(self) -> graphql.GraphQLSchema:
145
+ import graphql
146
+
98
147
  if not hasattr(self, "_client_schema"):
99
- # pylint: disable=attribute-defined-outside-init
100
148
  self._client_schema = graphql.build_client_schema(self.raw_schema)
101
149
  return self._client_schema
102
150
 
103
151
  @property
104
152
  def base_path(self) -> str:
105
- if self.base_url:
106
- return urlsplit(self.base_url).path
153
+ if self.config.base_url:
154
+ return urlsplit(self.config.base_url).path
107
155
  return self._get_base_path()
108
156
 
109
157
  def _get_base_path(self) -> str:
110
158
  return cast(str, urlsplit(self.location).path)
111
159
 
112
- @property
113
- def operations_count(self) -> int:
160
+ def _measure_statistic(self) -> ApiStatistic:
161
+ statistic = ApiStatistic()
114
162
  raw_schema = self.raw_schema["__schema"]
115
- total = 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
+
116
174
  for type_name in ("queryType", "mutationType"):
117
175
  type_def = raw_schema.get(type_name)
118
176
  if type_def is not None:
119
177
  query_type_name = type_def["name"]
120
178
  for type_def in raw_schema.get("types", []):
121
179
  if type_def["name"] == query_type_name:
122
- total += len(type_def["fields"])
123
- return total
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
124
186
 
125
187
  def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
126
188
  schema = self.client_schema
127
- for operation_type in (schema.query_type, schema.mutation_type):
189
+ for root_type, operation_type in (
190
+ (RootType.QUERY, schema.query_type),
191
+ (RootType.MUTATION, schema.mutation_type),
192
+ ):
128
193
  if operation_type is None:
129
194
  continue
130
- for field_name, definition in operation_type.fields.items():
131
- yield Ok(
132
- APIOperation(
133
- base_url=self.get_base_url(),
134
- path=self.base_path,
135
- verbose_name=f"{operation_type.name}.{field_name}",
136
- method="POST",
137
- app=self.app,
138
- schema=self,
139
- # Parameters are not yet supported
140
- definition=GraphQLOperationDefinition(
141
- raw=definition,
142
- resolved=definition,
143
- scope="",
144
- parameters=[],
145
- type_=operation_type,
146
- field_name=field_name,
147
- ),
148
- case_cls=GraphQLCase,
149
- )
150
- )
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
+ )
151
233
 
152
234
  def get_case_strategy(
153
235
  self,
154
236
  operation: APIOperation,
155
- hooks: Optional[HookDispatcher] = None,
156
- auth_storage: Optional[AuthStorage] = None,
157
- 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,
158
241
  ) -> SearchStrategy:
159
- return get_case_strategy(
242
+ return graphql_cases(
160
243
  operation=operation,
161
- client_schema=self.client_schema,
162
244
  hooks=hooks,
163
245
  auth_storage=auth_storage,
164
- data_generation_method=data_generation_method,
246
+ generation_mode=generation_mode,
247
+ **kwargs,
165
248
  )
166
249
 
167
- def get_strategies_from_examples(self, operation: APIOperation) -> List[SearchStrategy[Case]]:
168
- return []
169
-
170
- def get_stateful_tests(
171
- self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
172
- ) -> Sequence[StatefulTest]:
250
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
173
251
  return []
174
252
 
175
253
  def make_case(
176
254
  self,
177
255
  *,
178
- case_cls: Type[C],
179
256
  operation: APIOperation,
180
- path_parameters: Optional[PathParameters] = None,
181
- headers: Optional[Headers] = None,
182
- cookies: Optional[Cookies] = None,
183
- query: Optional[Query] = None,
184
- body: Union[Body, NotSet] = NOT_SET,
185
- media_type: Optional[str] = None,
186
- ) -> C:
187
- 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(
188
268
  operation=operation,
189
- path_parameters=path_parameters,
190
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
191
- cookies=cookies,
192
- 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 {},
193
275
  body=body,
194
- media_type=media_type,
276
+ media_type=media_type or "application/json",
277
+ meta=meta,
195
278
  )
196
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_)
197
311
 
198
- @st.composite # type: ignore
199
- def get_case_strategy(
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(
200
326
  draw: Callable,
327
+ *,
201
328
  operation: APIOperation,
202
- client_schema: graphql.GraphQLSchema,
203
- hooks: Optional[HookDispatcher] = None,
204
- auth_storage: Optional[AuthStorage] = None,
205
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
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,
206
339
  ) -> Any:
340
+ import graphql
341
+ from hypothesis_graphql import strategies as gql_st
342
+
343
+ start = time.monotonic()
207
344
  definition = cast(GraphQLOperationDefinition, operation.definition)
208
- strategy = {
209
- "Query": gql_st.queries,
210
- "Mutation": gql_st.mutations,
211
- }[definition.type_.name]
212
- body = draw(strategy(client_schema, fields=[definition.field_name], custom_scalars=CUSTOM_SCALARS))
213
- instance = GraphQLCase(body=body, operation=operation, data_generation_method=data_generation_method) # type: ignore
214
- context = auth.AuthContext(
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(
215
413
  operation=operation,
216
414
  app=operation.app,
217
415
  )
218
- auth.set_on_case(instance, context, auth_storage)
416
+ auths.set_on_case(instance, context, auth_storage)
219
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
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, cast
4
+
5
+ from schemathesis.generation.case import Case
6
+ from schemathesis.graphql.checks import GraphQLClientError, GraphQLServerError, UnexpectedGraphQLResponse
7
+
8
+
9
+ def validate_graphql_response(case: Case, payload: Any) -> None:
10
+ """Validate GraphQL response.
11
+
12
+ Semantically valid GraphQL responses are JSON objects and may contain `data` or `errors` keys.
13
+ """
14
+ from graphql.error import GraphQLFormattedError
15
+
16
+ if not isinstance(payload, dict):
17
+ raise UnexpectedGraphQLResponse(
18
+ operation=case.operation.label,
19
+ message="GraphQL response is not a JSON object",
20
+ type_name=str(type(payload)),
21
+ )
22
+
23
+ errors = cast(List[GraphQLFormattedError], payload.get("errors"))
24
+ if errors is not None and len(errors) > 0:
25
+ data = payload.get("data")
26
+ # There is no `path` pointing to some part of the input query, assuming client error
27
+ if data is None and "path" not in errors[0]:
28
+ raise GraphQLClientError(operation=case.operation.label, message=errors[0]["message"], errors=errors)
29
+ if len(errors) > 1:
30
+ message = "\n\n".join([f"{idx}. {error['message']}" for idx, error in enumerate(errors, 1)])
31
+ else:
32
+ message = errors[0]["message"]
33
+ raise GraphQLServerError(operation=case.operation.label, message=message, errors=errors)
@@ -1 +1,9 @@
1
- from .loaders import from_aiohttp, from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
1
+ from .formats import register_string_format as format
2
+ from .formats import unregister_string_format
3
+ from .media_types import register_media_type as media_type
4
+
5
+ __all__ = [
6
+ "format",
7
+ "unregister_string_format",
8
+ "media_type",
9
+ ]