schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,173 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import asdict
4
- from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, cast
5
-
6
- from ..internal.transformation import merge_recursively
7
- from ..runner import events
8
- from ..runner.serialization import _serialize_check
9
-
10
- if TYPE_CHECKING:
11
- from ..stateful import events as stateful_events
12
-
13
- S = TypeVar("S", bound=events.ExecutionEvent)
14
- SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
15
-
16
-
17
- def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
18
- return {
19
- "operations_count": event.operations_count,
20
- "location": event.location or "",
21
- "base_url": event.base_url,
22
- }
23
-
24
-
25
- def serialize_before_probing(_: events.BeforeProbing) -> None:
26
- return None
27
-
28
-
29
- def serialize_after_probing(event: events.AfterProbing) -> dict[str, Any] | None:
30
- probes = event.probes or []
31
- return {"probes": [probe.serialize() for probe in probes]}
32
-
33
-
34
- def serialize_before_analysis(_: events.BeforeAnalysis) -> None:
35
- return None
36
-
37
-
38
- def serialize_after_analysis(event: events.AfterAnalysis) -> dict[str, Any] | None:
39
- return event._serialize()
40
-
41
-
42
- def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
43
- return {
44
- "correlation_id": event.correlation_id,
45
- "verbose_name": event.verbose_name,
46
- "data_generation_method": event.data_generation_method,
47
- }
48
-
49
-
50
- def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
51
- return {
52
- "correlation_id": event.correlation_id,
53
- "verbose_name": event.verbose_name,
54
- "status": event.status,
55
- "elapsed_time": event.elapsed_time,
56
- "data_generation_method": event.data_generation_method,
57
- "result": {
58
- "checks": [_serialize_check(check) for check in event.result.checks],
59
- "errors": [asdict(error) for error in event.result.errors],
60
- "skip_reason": event.result.skip_reason,
61
- },
62
- }
63
-
64
-
65
- def serialize_interrupted(_: events.Interrupted) -> dict[str, Any] | None:
66
- return None
67
-
68
-
69
- def serialize_internal_error(event: events.InternalError) -> dict[str, Any] | None:
70
- return {
71
- "type": event.type.value,
72
- "subtype": event.subtype.value if event.subtype else event.subtype,
73
- "title": event.title,
74
- "message": event.message,
75
- "extras": event.extras,
76
- "exception_type": event.exception_type,
77
- "exception": event.exception,
78
- "exception_with_traceback": event.exception_with_traceback,
79
- }
80
-
81
-
82
- def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
83
- return {
84
- "generic_errors": [
85
- {
86
- "exception": error.exception,
87
- "exception_with_traceback": error.exception_with_traceback,
88
- "title": error.title,
89
- }
90
- for error in event.generic_errors
91
- ],
92
- "running_time": event.running_time,
93
- }
94
-
95
-
96
- def serialize_stateful_event(event: events.StatefulEvent) -> dict[str, Any] | None:
97
- return _serialize_stateful_event(event.data)
98
-
99
-
100
- def _serialize_stateful_event(event: stateful_events.StatefulEvent) -> dict[str, Any] | None:
101
- return {"data": {event.__class__.__name__: event.asdict()}}
102
-
103
-
104
- def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
105
- return {
106
- "status": event.status,
107
- "data_generation_method": event.data_generation_method,
108
- "result": asdict(event.result),
109
- "elapsed_time": event.elapsed_time,
110
- }
111
-
112
-
113
- SERIALIZER_MAP = {
114
- events.Initialized: serialize_initialized,
115
- events.BeforeProbing: serialize_before_probing,
116
- events.AfterProbing: serialize_after_probing,
117
- events.BeforeAnalysis: serialize_before_analysis,
118
- events.AfterAnalysis: serialize_after_analysis,
119
- events.BeforeExecution: serialize_before_execution,
120
- events.AfterExecution: serialize_after_execution,
121
- events.Interrupted: serialize_interrupted,
122
- events.InternalError: serialize_internal_error,
123
- events.StatefulEvent: serialize_stateful_event,
124
- events.AfterStatefulExecution: serialize_after_stateful_execution,
125
- events.Finished: serialize_finished,
126
- }
127
-
128
-
129
- def serialize_event(
130
- event: events.ExecutionEvent,
131
- *,
132
- on_initialized: SerializeFunc | None = None,
133
- on_before_probing: SerializeFunc | None = None,
134
- on_after_probing: SerializeFunc | None = None,
135
- on_before_analysis: SerializeFunc | None = None,
136
- on_after_analysis: SerializeFunc | None = None,
137
- on_before_execution: SerializeFunc | None = None,
138
- on_after_execution: SerializeFunc | None = None,
139
- on_interrupted: SerializeFunc | None = None,
140
- on_internal_error: SerializeFunc | None = None,
141
- on_stateful_event: SerializeFunc | None = None,
142
- on_after_stateful_execution: SerializeFunc | None = None,
143
- on_finished: SerializeFunc | None = None,
144
- extra: dict[str, Any] | None = None,
145
- ) -> dict[str, dict[str, Any] | None]:
146
- """Turn an event into JSON-serializable structure."""
147
- # Use the explicitly provided serializer for this event and fallback to default one if it is not provided
148
- serializer = {
149
- events.Initialized: on_initialized,
150
- events.BeforeProbing: on_before_probing,
151
- events.AfterProbing: on_after_probing,
152
- events.BeforeAnalysis: on_before_analysis,
153
- events.AfterAnalysis: on_after_analysis,
154
- events.BeforeExecution: on_before_execution,
155
- events.AfterExecution: on_after_execution,
156
- events.Interrupted: on_interrupted,
157
- events.InternalError: on_internal_error,
158
- events.StatefulEvent: on_stateful_event,
159
- events.AfterStatefulExecution: on_after_stateful_execution,
160
- events.Finished: on_finished,
161
- }.get(event.__class__)
162
- if serializer is None:
163
- serializer = cast(SerializeFunc, SERIALIZER_MAP[event.__class__])
164
- data = serializer(event)
165
- if extra is not None:
166
- # If `extra` is present, then merge it with the serialized data. If serialized data is empty, then replace it
167
- # with `extra` value
168
- if data is None:
169
- data = extra
170
- else:
171
- data = merge_recursively(data, extra)
172
- # Externally tagged structure
173
- return {event.__class__.__name__: data}
@@ -1,66 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from typing import Any
5
-
6
- import click
7
- from click.types import StringParamType
8
-
9
- from .. import cli, hooks
10
-
11
-
12
- def collect(args: list[str] | None = None) -> dict[str, Any] | None:
13
- """Collect anonymized CLI usage data."""
14
- context: click.Context | None = click.get_current_context(silent=True)
15
- if context is not None and not sys.argv[0].endswith("pytest"):
16
- args = args or sys.argv[2:]
17
- parameters, _, types = parse_cli_args(context, args)
18
- parameters_data: dict[str, dict[str, Any]] = {}
19
- used_headers: list[str] = []
20
- schema = parameters["schema"]
21
- app = parameters.get("app")
22
- if not schema:
23
- schema_kind = None
24
- else:
25
- schema_kind = cli.callbacks.parse_schema_kind(schema, app).name
26
- usage = {
27
- "schema_kind": schema_kind,
28
- "parameters": parameters_data,
29
- "used_headers": used_headers,
30
- "hooks": hooks.collect_statistic(),
31
- }
32
- types_iter = iter(types)
33
- for option, value in parameters.items():
34
- option_type = next(types_iter)
35
- if isinstance(option_type, click.Argument):
36
- continue
37
- if option_type.multiple:
38
- # Forward the iterator to the next option type
39
- for _ in range(len(value) - 1):
40
- next(types_iter)
41
- entry = _collect_option(option, option_type, used_headers, value)
42
- if entry:
43
- parameters_data[option] = entry
44
- return usage
45
- return None
46
-
47
-
48
- def _collect_option(option: str, option_type: click.Parameter, used_headers: list[str], value: Any) -> dict[str, Any]:
49
- entry = {}
50
- if isinstance(option_type.type, (StringParamType, click.types.File)):
51
- if option == "headers" and value:
52
- used_headers.extend(header.split(":", 1)[0] for header in value)
53
- else:
54
- # Free-form values are replaced with their number of occurrences, to avoid sending sensitive info
55
- if option_type.multiple:
56
- entry["count"] = len(value)
57
- else:
58
- entry["count"] = 1
59
- else:
60
- entry["value"] = value
61
- return entry
62
-
63
-
64
- def parse_cli_args(context: click.Context, args: list[str]) -> tuple[dict[str, Any], list, list[click.Parameter]]:
65
- parser = cli.run.make_parser(context)
66
- return parser.parse_args(args=args)
@@ -1,364 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import pathlib
5
- from functools import lru_cache
6
- from json import JSONDecodeError
7
- from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, cast
8
-
9
- from ...code_samples import CodeSampleStyle
10
- from ...constants import DEFAULT_RESPONSE_TIMEOUT, WAIT_FOR_SCHEMA_INTERVAL
11
- from ...exceptions import SchemaError, SchemaErrorType
12
- from ...generation import (
13
- DEFAULT_DATA_GENERATION_METHODS,
14
- DataGenerationMethod,
15
- DataGenerationMethodInput,
16
- GenerationConfig,
17
- )
18
- from ...hooks import HookContext, dispatch
19
- from ...internal.output import OutputConfig
20
- from ...internal.validation import require_relative_url
21
- from ...loaders import load_schema_from_url
22
- from ...throttling import build_limiter
23
- from ...transports.headers import setup_default_headers
24
- from ...types import PathLike, Specification
25
-
26
- if TYPE_CHECKING:
27
- from graphql import DocumentNode
28
- from pyrate_limiter import Limiter
29
-
30
- from ...transports.responses import GenericResponse
31
- from .schemas import GraphQLSchema
32
-
33
-
34
- @lru_cache
35
- def get_introspection_query() -> str:
36
- import graphql
37
-
38
- return graphql.get_introspection_query()
39
-
40
-
41
- @lru_cache
42
- def get_introspection_query_ast() -> DocumentNode:
43
- import graphql
44
-
45
- query = get_introspection_query()
46
- return graphql.parse(query)
47
-
48
-
49
- def from_path(
50
- path: PathLike,
51
- *,
52
- app: Any = None,
53
- base_url: str | None = None,
54
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
55
- generation_config: GenerationConfig | None = None,
56
- output_config: OutputConfig | None = None,
57
- code_sample_style: str = CodeSampleStyle.default().name,
58
- rate_limit: str | None = None,
59
- encoding: str = "utf8",
60
- sanitize_output: bool = True,
61
- ) -> GraphQLSchema:
62
- """Load GraphQL schema via a file from an OS path.
63
-
64
- :param path: A path to the schema file.
65
- :param encoding: The name of the encoding used to decode the file.
66
- """
67
- with open(path, encoding=encoding) as fd:
68
- return from_file(
69
- fd,
70
- app=app,
71
- base_url=base_url,
72
- data_generation_methods=data_generation_methods,
73
- code_sample_style=code_sample_style,
74
- generation_config=generation_config,
75
- output_config=output_config,
76
- location=pathlib.Path(path).absolute().as_uri(),
77
- rate_limit=rate_limit,
78
- sanitize_output=sanitize_output,
79
- )
80
-
81
-
82
- def extract_schema_from_response(response: GenericResponse) -> dict[str, Any]:
83
- from requests import Response
84
-
85
- try:
86
- if isinstance(response, Response):
87
- decoded = response.json()
88
- else:
89
- decoded = response.json
90
- except JSONDecodeError as exc:
91
- raise SchemaError(
92
- SchemaErrorType.UNEXPECTED_CONTENT_TYPE,
93
- "Received unsupported content while expecting a JSON payload for GraphQL",
94
- ) from exc
95
- return decoded
96
-
97
-
98
- def from_url(
99
- url: str,
100
- *,
101
- app: Any = None,
102
- base_url: str | None = None,
103
- port: int | None = None,
104
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
105
- generation_config: GenerationConfig | None = None,
106
- code_sample_style: str = CodeSampleStyle.default().name,
107
- wait_for_schema: float | None = None,
108
- rate_limit: str | None = None,
109
- sanitize_output: bool = True,
110
- **kwargs: Any,
111
- ) -> GraphQLSchema:
112
- """Load GraphQL schema from the network.
113
-
114
- :param url: Schema URL.
115
- :param Optional[str] base_url: Base URL to send requests to.
116
- :param Optional[int] port: An optional port if you don't want to pass the ``base_url`` parameter, but only to change
117
- port in ``url``.
118
- :param app: A WSGI app instance.
119
- :return: GraphQLSchema
120
- """
121
- import backoff
122
- import requests
123
-
124
- setup_default_headers(kwargs)
125
- kwargs.setdefault("json", {"query": get_introspection_query()})
126
- if port:
127
- from yarl import URL
128
-
129
- url = str(URL(url).with_port(port))
130
- if not base_url:
131
- base_url = url
132
-
133
- if wait_for_schema is not None:
134
-
135
- @backoff.on_exception( # type: ignore
136
- backoff.constant,
137
- requests.exceptions.ConnectionError,
138
- max_time=wait_for_schema,
139
- interval=WAIT_FOR_SCHEMA_INTERVAL,
140
- )
141
- def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
142
- return requests.post(_uri, **_kwargs)
143
-
144
- else:
145
- _load_schema = requests.post
146
-
147
- kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
148
- response = load_schema_from_url(lambda: _load_schema(url, **kwargs))
149
- raw_schema = extract_schema_from_response(response)
150
- return from_dict(
151
- raw_schema=raw_schema,
152
- location=url,
153
- base_url=base_url,
154
- app=app,
155
- data_generation_methods=data_generation_methods,
156
- code_sample_style=code_sample_style,
157
- rate_limit=rate_limit,
158
- sanitize_output=sanitize_output,
159
- )
160
-
161
-
162
- def from_file(
163
- file: IO[str] | str,
164
- *,
165
- app: Any = None,
166
- base_url: str | None = None,
167
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
168
- generation_config: GenerationConfig | None = None,
169
- output_config: OutputConfig | None = None,
170
- code_sample_style: str = CodeSampleStyle.default().name,
171
- location: str | None = None,
172
- rate_limit: str | None = None,
173
- sanitize_output: bool = True,
174
- ) -> GraphQLSchema:
175
- """Load GraphQL schema from a file descriptor or a string.
176
-
177
- :param file: Could be a file descriptor, string or bytes.
178
- """
179
- import graphql
180
-
181
- if isinstance(file, str):
182
- data = file
183
- else:
184
- data = file.read()
185
- try:
186
- document = graphql.build_schema(data)
187
- result = graphql.execute(document, get_introspection_query_ast())
188
- # TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
189
- result = cast(graphql.ExecutionResult, result)
190
- # TYPES:
191
- # - `document` is a valid schema, because otherwise `build_schema` will rise an error;
192
- # - `INTROSPECTION_QUERY` is a valid query - it is known upfront;
193
- # Therefore the execution result is always valid at this point and `result.data` is not `None`
194
- raw_schema = cast(Dict[str, Any], result.data)
195
- except Exception as exc:
196
- try:
197
- raw_schema = json.loads(data)
198
- if not isinstance(raw_schema, dict) or "__schema" not in raw_schema:
199
- _on_invalid_schema(exc)
200
- except json.JSONDecodeError:
201
- _on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
202
- return from_dict(
203
- raw_schema,
204
- app=app,
205
- base_url=base_url,
206
- data_generation_methods=data_generation_methods,
207
- generation_config=generation_config,
208
- output_config=output_config,
209
- code_sample_style=code_sample_style,
210
- location=location,
211
- rate_limit=rate_limit,
212
- sanitize_output=sanitize_output,
213
- )
214
-
215
-
216
- def _on_invalid_schema(exc: Exception, extras: list[str] | None = None) -> NoReturn:
217
- raise SchemaError(
218
- SchemaErrorType.GRAPHQL_INVALID_SCHEMA,
219
- "The provided API schema does not appear to be a valid GraphQL schema",
220
- extras=extras or [],
221
- ) from exc
222
-
223
-
224
- def from_dict(
225
- raw_schema: dict[str, Any],
226
- *,
227
- app: Any = None,
228
- base_url: str | None = None,
229
- location: str | None = None,
230
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
231
- generation_config: GenerationConfig | None = None,
232
- output_config: OutputConfig | None = None,
233
- code_sample_style: str = CodeSampleStyle.default().name,
234
- rate_limit: str | None = None,
235
- sanitize_output: bool = True,
236
- ) -> GraphQLSchema:
237
- """Load GraphQL schema from a Python dictionary.
238
-
239
- :param dict raw_schema: A schema to load.
240
- :param Optional[str] location: Optional schema location. Either a full URL or a filesystem path.
241
- :param Optional[str] base_url: Base URL to send requests to.
242
- :param app: A WSGI app instance.
243
- :return: GraphQLSchema
244
- """
245
- from ... import transports
246
- from .schemas import GraphQLSchema
247
-
248
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
249
- hook_context = HookContext()
250
- if "data" in raw_schema:
251
- raw_schema = raw_schema["data"]
252
- dispatch("before_load_schema", hook_context, raw_schema)
253
- rate_limiter: Limiter | None = None
254
- if rate_limit is not None:
255
- rate_limiter = build_limiter(rate_limit)
256
- instance = GraphQLSchema(
257
- raw_schema,
258
- specification=Specification.GRAPHQL,
259
- location=location,
260
- base_url=base_url,
261
- app=app,
262
- data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
263
- generation_config=generation_config or GenerationConfig(),
264
- output_config=output_config or OutputConfig(),
265
- code_sample_style=_code_sample_style,
266
- rate_limiter=rate_limiter,
267
- sanitize_output=sanitize_output,
268
- transport=transports.get(app),
269
- ) # type: ignore
270
- dispatch("after_load_schema", hook_context, instance)
271
- return instance
272
-
273
-
274
- def from_wsgi(
275
- schema_path: str,
276
- app: Any,
277
- *,
278
- base_url: str | None = None,
279
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
280
- generation_config: GenerationConfig | None = None,
281
- output_config: OutputConfig | None = None,
282
- code_sample_style: str = CodeSampleStyle.default().name,
283
- rate_limit: str | None = None,
284
- sanitize_output: bool = True,
285
- **kwargs: Any,
286
- ) -> GraphQLSchema:
287
- """Load GraphQL schema from a WSGI app.
288
-
289
- :param str schema_path: An in-app relative URL to the schema.
290
- :param app: A WSGI app instance.
291
- :param Optional[str] base_url: Base URL to send requests to.
292
- :return: GraphQLSchema
293
- """
294
- from werkzeug import Client
295
-
296
- from ...transports.responses import WSGIResponse
297
-
298
- require_relative_url(schema_path)
299
- setup_default_headers(kwargs)
300
- kwargs.setdefault("json", {"query": get_introspection_query()})
301
- client = Client(app, WSGIResponse)
302
- response = load_schema_from_url(lambda: client.post(schema_path, **kwargs))
303
- raw_schema = extract_schema_from_response(response)
304
- return from_dict(
305
- raw_schema=raw_schema,
306
- location=schema_path,
307
- base_url=base_url,
308
- app=app,
309
- data_generation_methods=data_generation_methods,
310
- generation_config=generation_config,
311
- output_config=output_config,
312
- code_sample_style=code_sample_style,
313
- rate_limit=rate_limit,
314
- sanitize_output=sanitize_output,
315
- )
316
-
317
-
318
- def from_asgi(
319
- schema_path: str,
320
- app: Any,
321
- *,
322
- base_url: str | None = None,
323
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
324
- generation_config: GenerationConfig | None = None,
325
- output_config: OutputConfig | None = None,
326
- code_sample_style: str = CodeSampleStyle.default().name,
327
- rate_limit: str | None = None,
328
- sanitize_output: bool = True,
329
- **kwargs: Any,
330
- ) -> GraphQLSchema:
331
- """Load GraphQL schema from an ASGI app.
332
-
333
- :param str schema_path: An in-app relative URL to the schema.
334
- :param app: An ASGI app instance.
335
- :param Optional[str] base_url: Base URL to send requests to.
336
- """
337
- from starlette_testclient import TestClient as ASGIClient
338
-
339
- require_relative_url(schema_path)
340
- setup_default_headers(kwargs)
341
- kwargs.setdefault("json", {"query": get_introspection_query()})
342
- client = ASGIClient(app)
343
- response = load_schema_from_url(lambda: client.post(schema_path, **kwargs))
344
- raw_schema = extract_schema_from_response(response)
345
- return from_dict(
346
- raw_schema=raw_schema,
347
- location=schema_path,
348
- base_url=base_url,
349
- app=app,
350
- data_generation_methods=data_generation_methods,
351
- generation_config=generation_config,
352
- output_config=output_config,
353
- code_sample_style=code_sample_style,
354
- rate_limit=rate_limit,
355
- sanitize_output=sanitize_output,
356
- )
357
-
358
-
359
- def get_loader_for_app(app: Any) -> Callable:
360
- from ...transports.asgi import is_asgi_app
361
-
362
- if is_asgi_app(app):
363
- return from_asgi
364
- return from_wsgi
@@ -1,16 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from ....models import Case
8
- from ....transports.responses import GenericResponse
9
-
10
-
11
- @dataclass
12
- class ExpressionContext:
13
- """Context in what an expression are evaluated."""
14
-
15
- response: GenericResponse
16
- case: Case