schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import re
5
+ from ipaddress import IPv4Network, IPv6Network
6
+ from typing import TYPE_CHECKING, Any, Callable
7
+
8
+ from ..graphql import nodes
9
+ from ..internal.result import Err, Ok, Result
10
+ from .models import (
11
+ Extension,
12
+ GraphQLScalarsExtension,
13
+ MediaTypesExtension,
14
+ OpenApiStringFormatsExtension,
15
+ SchemaPatchesExtension,
16
+ StrategyDefinition,
17
+ TransformFunctionDefinition,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from datetime import date, datetime
22
+
23
+ from hypothesis import strategies as st
24
+
25
+ from ..schemas import BaseSchema
26
+
27
+
28
+ def apply(extensions: list[Extension], schema: BaseSchema) -> None:
29
+ """Apply the given extensions."""
30
+ for extension in extensions:
31
+ if isinstance(extension, OpenApiStringFormatsExtension):
32
+ _apply_string_formats_extension(extension)
33
+ elif isinstance(extension, GraphQLScalarsExtension):
34
+ _apply_scalars_extension(extension)
35
+ elif isinstance(extension, MediaTypesExtension):
36
+ _apply_media_types_extension(extension)
37
+ elif isinstance(extension, SchemaPatchesExtension):
38
+ _apply_schema_patches_extension(extension, schema)
39
+
40
+
41
+ def _apply_simple_extension(
42
+ extension: OpenApiStringFormatsExtension | GraphQLScalarsExtension | MediaTypesExtension,
43
+ collection: dict[str, Any],
44
+ register_strategy: Callable[[str, st.SearchStrategy], None],
45
+ ) -> None:
46
+ errors = []
47
+ for name, value in collection.items():
48
+ strategy = strategy_from_definitions(value)
49
+ if isinstance(strategy, Err):
50
+ errors.append(str(strategy.err()))
51
+ else:
52
+ register_strategy(name, strategy.ok())
53
+
54
+ if errors:
55
+ extension.set_error(errors=errors)
56
+ else:
57
+ extension.set_success()
58
+
59
+
60
+ def _apply_string_formats_extension(extension: OpenApiStringFormatsExtension) -> None:
61
+ from ..specs.openapi import formats
62
+
63
+ _apply_simple_extension(extension, extension.formats, formats.register)
64
+
65
+
66
+ def _apply_scalars_extension(extension: GraphQLScalarsExtension) -> None:
67
+ from ..specs.graphql import scalars
68
+
69
+ _apply_simple_extension(extension, extension.scalars, scalars.scalar)
70
+
71
+
72
+ def _apply_media_types_extension(extension: MediaTypesExtension) -> None:
73
+ from ..specs.openapi import media_types
74
+
75
+ _apply_simple_extension(extension, extension.media_types, media_types.register_media_type)
76
+
77
+
78
+ def _find_built_in_strategy(name: str) -> st.SearchStrategy | None:
79
+ """Find a built-in Hypothesis strategy by its name."""
80
+ from hypothesis import provisional as pr
81
+ from hypothesis import strategies as st
82
+
83
+ for module in (st, pr):
84
+ if hasattr(module, name):
85
+ return getattr(module, name)
86
+ return None
87
+
88
+
89
+ def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: BaseSchema) -> None:
90
+ """Apply a set of patches to the schema."""
91
+ for patch in extension.patches:
92
+ current: dict[str, Any] | list = schema.raw_schema
93
+ operation = patch["operation"]
94
+ path = patch["path"]
95
+ for part in path[:-1]:
96
+ if isinstance(current, dict):
97
+ if not isinstance(part, str):
98
+ extension.set_error([f"Invalid path: {path}"])
99
+ return
100
+ current = current.setdefault(part, {})
101
+ elif isinstance(current, list):
102
+ if not isinstance(part, int):
103
+ extension.set_error([f"Invalid path: {path}"])
104
+ return
105
+ try:
106
+ current = current[part]
107
+ except IndexError:
108
+ extension.set_error([f"Invalid path: {path}"])
109
+ return
110
+ if operation == "add":
111
+ # Add or replace the value at the target location.
112
+ current[path[-1]] = patch["value"] # type: ignore
113
+ elif operation == "remove":
114
+ # Remove the item at the target location if it exists.
115
+ if path:
116
+ last = path[-1]
117
+ if isinstance(current, dict) and isinstance(last, str) and last in current:
118
+ del current[last]
119
+ elif isinstance(current, list) and isinstance(last, int) and len(current) > last:
120
+ del current[last]
121
+ else:
122
+ extension.set_error([f"Invalid path: {path}"])
123
+ return
124
+ else:
125
+ current.clear()
126
+
127
+ extension.set_success()
128
+
129
+
130
+ def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
131
+ from ..generation import combine_strategies
132
+
133
+ strategies = []
134
+ for definition in definitions:
135
+ strategy = _strategy_from_definition(definition)
136
+ if isinstance(strategy, Ok):
137
+ strategies.append(strategy.ok())
138
+ else:
139
+ return strategy
140
+ return Ok(combine_strategies(strategies))
141
+
142
+
143
+ KNOWN_ARGUMENTS = {
144
+ "IPv4Network": IPv4Network,
145
+ "IPv6Network": IPv6Network,
146
+ }
147
+
148
+
149
+ def check_regex(regex: str) -> Result[None, Exception]:
150
+ try:
151
+ re.compile(regex)
152
+ except (re.error, OverflowError, RuntimeError):
153
+ return Err(ValueError(f"Invalid regex: `{regex}`"))
154
+ return Ok(None)
155
+
156
+
157
+ def check_sampled_from(elements: list) -> Result[None, Exception]:
158
+ if not elements:
159
+ return Err(ValueError("Invalid input for `sampled_from`: Cannot sample from a length-zero sequence"))
160
+ return Ok(None)
161
+
162
+
163
+ STRATEGY_ARGUMENT_CHECKS = {
164
+ "from_regex": check_regex,
165
+ "sampled_from": check_sampled_from,
166
+ }
167
+
168
+
169
+ def _strategy_from_definition(definition: StrategyDefinition) -> Result[st.SearchStrategy, Exception]:
170
+ base = _find_built_in_strategy(definition.name)
171
+ if base is None:
172
+ return Err(ValueError(f"Unknown built-in strategy: `{definition.name}`"))
173
+ arguments = definition.arguments or {}
174
+ arguments = arguments.copy()
175
+ for key, value in arguments.items():
176
+ if isinstance(value, str):
177
+ known = KNOWN_ARGUMENTS.get(value)
178
+ if known is not None:
179
+ arguments[key] = known
180
+ check = STRATEGY_ARGUMENT_CHECKS.get(definition.name)
181
+ if check is not None:
182
+ check_result = check(**arguments) # type: ignore
183
+ if isinstance(check_result, Err):
184
+ return check_result
185
+ strategy = base(**arguments)
186
+ for transform in definition.transforms or []:
187
+ if transform["kind"] == "map":
188
+ function = _get_map_function(transform)
189
+ if isinstance(function, Ok):
190
+ strategy = strategy.map(function.ok())
191
+ else:
192
+ return function
193
+ else:
194
+ return Err(ValueError(f"Unknown transform kind: {transform['kind']}"))
195
+
196
+ return Ok(strategy)
197
+
198
+
199
+ def make_strftime(format: str) -> Callable:
200
+ def strftime(value: date | datetime) -> str:
201
+ return value.strftime(format)
202
+
203
+ return strftime
204
+
205
+
206
+ def _get_map_function(definition: TransformFunctionDefinition) -> Result[Callable | None, Exception]:
207
+ from ..serializers import Binary
208
+
209
+ TRANSFORM_FACTORIES: dict[str, Callable] = {
210
+ "str": lambda: str,
211
+ "base64_encode": lambda: lambda x: Binary(base64.b64encode(x)),
212
+ "base64_decode": lambda: lambda x: Binary(base64.b64decode(x)),
213
+ "urlsafe_base64_encode": lambda: lambda x: Binary(base64.urlsafe_b64encode(x)),
214
+ "strftime": make_strftime,
215
+ "GraphQLBoolean": lambda: nodes.Boolean,
216
+ "GraphQLFloat": lambda: nodes.Float,
217
+ "GraphQLInt": lambda: nodes.Int,
218
+ "GraphQLString": lambda: nodes.String,
219
+ }
220
+ factory = TRANSFORM_FACTORIES.get(definition["name"])
221
+ if factory is None:
222
+ return Err(ValueError(f"Unknown transform: {definition['name']}"))
223
+ arguments = definition.get("arguments", {})
224
+ return Ok(factory(**arguments))
@@ -1,17 +1,21 @@
1
1
  """Work with stored auth data."""
2
+
2
3
  from __future__ import annotations
4
+
3
5
  import enum
4
6
  import tempfile
5
7
  from dataclasses import dataclass
6
8
  from pathlib import Path
7
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
8
10
 
9
11
  import tomli
10
12
  import tomli_w
11
13
 
12
- from ..types import PathLike
13
14
  from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
14
15
 
16
+ if TYPE_CHECKING:
17
+ from ..types import PathLike
18
+
15
19
 
16
20
  @dataclass
17
21
  class HostData:
@@ -1,8 +1,11 @@
1
1
  """Useful info to collect from CLI usage."""
2
+
2
3
  from __future__ import annotations
4
+
3
5
  import os
4
6
  import platform
5
7
  from dataclasses import dataclass, field
8
+ from importlib import metadata
6
9
 
7
10
  from ..constants import SCHEMATHESIS_VERSION
8
11
  from .constants import DOCKER_IMAGE_ENV_VAR
@@ -32,6 +35,27 @@ class CliMetadata:
32
35
  version: str = SCHEMATHESIS_VERSION
33
36
 
34
37
 
38
+ DEPENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
39
+
40
+
41
+ @dataclass
42
+ class Dependency:
43
+ """A single dependency."""
44
+
45
+ # Name of the package.
46
+ name: str
47
+ # Version of the package.
48
+ version: str
49
+
50
+ @classmethod
51
+ def from_name(cls, name: str) -> Dependency:
52
+ return cls(name=name, version=metadata.version(name))
53
+
54
+
55
+ def collect_dependency_versions() -> list[Dependency]:
56
+ return [Dependency.from_name(name) for name in DEPENDENCY_NAMES]
57
+
58
+
35
59
  @dataclass
36
60
  class Metadata:
37
61
  """CLI environment metadata."""
@@ -44,3 +68,4 @@ class Metadata:
44
68
  cli: CliMetadata = field(default_factory=CliMetadata)
45
69
  # Used Docker image if any
46
70
  docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
71
+ depedenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
- from dataclasses import dataclass
2
+
3
+ from dataclasses import dataclass, field
3
4
  from enum import Enum
4
- from typing import Any
5
+ from typing import Any, Iterable, Literal, TypedDict, Union
5
6
 
6
7
 
7
8
  class UploadSource(str, Enum):
@@ -47,3 +48,211 @@ class UploadResponse:
47
48
  @dataclass
48
49
  class FailedUploadResponse:
49
50
  detail: str
51
+
52
+
53
+ @dataclass
54
+ class NotAppliedState:
55
+ """The extension was not applied."""
56
+
57
+ def __str__(self) -> str:
58
+ return "Not Applied"
59
+
60
+
61
+ @dataclass
62
+ class SuccessState:
63
+ """The extension was applied successfully."""
64
+
65
+ def __str__(self) -> str:
66
+ return "Success"
67
+
68
+
69
+ @dataclass
70
+ class ErrorState:
71
+ """An error occurred during the extension application."""
72
+
73
+ errors: list[str] = field(default_factory=list)
74
+ exceptions: list[Exception] = field(default_factory=list)
75
+
76
+ def __str__(self) -> str:
77
+ return "Error"
78
+
79
+
80
+ ExtensionState = Union[NotAppliedState, SuccessState, ErrorState]
81
+
82
+
83
+ @dataclass
84
+ class BaseExtension:
85
+ def set_state(self, state: ExtensionState) -> None:
86
+ self.state = state
87
+
88
+ def set_success(self) -> None:
89
+ self.set_state(SuccessState())
90
+
91
+ def set_error(self, errors: list[str] | None = None, exceptions: list[Exception] | None = None) -> None:
92
+ self.set_state(ErrorState(errors=errors or [], exceptions=exceptions or []))
93
+
94
+
95
+ @dataclass
96
+ class UnknownExtension(BaseExtension):
97
+ """An unknown extension.
98
+
99
+ Likely the CLI should be updated.
100
+ """
101
+
102
+ type: str
103
+ state: ExtensionState = field(default_factory=NotAppliedState)
104
+
105
+ @property
106
+ def summary(self) -> str:
107
+ return f"`{self.type}`"
108
+
109
+
110
+ class AddPatch(TypedDict):
111
+ operation: Literal["add"]
112
+ path: list[str | int]
113
+ value: Any
114
+
115
+
116
+ class RemovePatch(TypedDict):
117
+ operation: Literal["remove"]
118
+ path: list[str | int]
119
+
120
+
121
+ Patch = Union[AddPatch, RemovePatch]
122
+
123
+
124
+ @dataclass
125
+ class SchemaPatchesExtension(BaseExtension):
126
+ """Update the schema with its optimized version."""
127
+
128
+ patches: list[Patch]
129
+ state: ExtensionState = field(default_factory=NotAppliedState)
130
+
131
+ @property
132
+ def summary(self) -> str:
133
+ count = len(self.patches)
134
+ plural = "es" if count > 1 else ""
135
+ return f"{count} schema patch{plural}"
136
+
137
+
138
+ class TransformFunctionDefinition(TypedDict):
139
+ kind: Literal["map", "filter"]
140
+ name: str
141
+ arguments: dict[str, Any]
142
+
143
+
144
+ @dataclass
145
+ class StrategyDefinition:
146
+ name: str
147
+ transforms: list[TransformFunctionDefinition] | None = None
148
+ arguments: dict[str, Any] | None = None
149
+
150
+
151
+ def _strategies_from_definition(items: dict[str, list[dict[str, Any]]]) -> dict[str, list[StrategyDefinition]]:
152
+ return {name: [StrategyDefinition(**item) for item in value] for name, value in items.items()}
153
+
154
+
155
+ def _format_items(items: Iterable[str]) -> str:
156
+ return ", ".join([f"`{item}`" for item in items])
157
+
158
+
159
+ @dataclass
160
+ class OpenApiStringFormatsExtension(BaseExtension):
161
+ """Custom string formats."""
162
+
163
+ formats: dict[str, list[StrategyDefinition]]
164
+ state: ExtensionState = field(default_factory=NotAppliedState)
165
+
166
+ @classmethod
167
+ def from_dict(cls, formats: dict[str, list[dict[str, Any]]]) -> OpenApiStringFormatsExtension:
168
+ return cls(formats=_strategies_from_definition(formats))
169
+
170
+ @property
171
+ def summary(self) -> str:
172
+ count = len(self.formats)
173
+ plural = "s" if count > 1 else ""
174
+ formats = _format_items(self.formats)
175
+ return f"Data generator{plural} for {formats} Open API format{plural}"
176
+
177
+
178
+ @dataclass
179
+ class GraphQLScalarsExtension(BaseExtension):
180
+ """Custom scalars."""
181
+
182
+ scalars: dict[str, list[StrategyDefinition]]
183
+ state: ExtensionState = field(default_factory=NotAppliedState)
184
+
185
+ @classmethod
186
+ def from_dict(cls, scalars: dict[str, list[dict[str, Any]]]) -> GraphQLScalarsExtension:
187
+ return cls(scalars=_strategies_from_definition(scalars))
188
+
189
+ @property
190
+ def summary(self) -> str:
191
+ count = len(self.scalars)
192
+ plural = "s" if count > 1 else ""
193
+ scalars = _format_items(self.scalars)
194
+ return f"Data generator{plural} for {scalars} GraphQL scalar{plural}"
195
+
196
+
197
+ @dataclass
198
+ class MediaTypesExtension(BaseExtension):
199
+ media_types: dict[str, list[StrategyDefinition]]
200
+ state: ExtensionState = field(default_factory=NotAppliedState)
201
+
202
+ @classmethod
203
+ def from_dict(cls, media_types: dict[str, list[dict[str, Any]]]) -> MediaTypesExtension:
204
+ return cls(media_types=_strategies_from_definition(media_types))
205
+
206
+ @property
207
+ def summary(self) -> str:
208
+ count = len(self.media_types)
209
+ plural = "s" if count > 1 else ""
210
+ media_types = _format_items(self.media_types)
211
+ return f"Data generator{plural} for {media_types} media type{plural}"
212
+
213
+
214
+ # A CLI extension that can be used to adjust the behavior of Schemathesis.
215
+ Extension = Union[
216
+ SchemaPatchesExtension,
217
+ OpenApiStringFormatsExtension,
218
+ GraphQLScalarsExtension,
219
+ MediaTypesExtension,
220
+ UnknownExtension,
221
+ ]
222
+
223
+
224
+ def extension_from_dict(data: dict[str, Any]) -> Extension:
225
+ if data["type"] == "schema_patches":
226
+ return SchemaPatchesExtension(patches=data["patches"])
227
+ if data["type"] == "string_formats":
228
+ return OpenApiStringFormatsExtension.from_dict(formats=data["items"])
229
+ if data["type"] == "scalars":
230
+ return GraphQLScalarsExtension.from_dict(scalars=data["items"])
231
+ if data["type"] == "media_types":
232
+ return MediaTypesExtension.from_dict(media_types=data["items"])
233
+ return UnknownExtension(type=data["type"])
234
+
235
+
236
+ @dataclass
237
+ class AnalysisSuccess:
238
+ id: str
239
+ elapsed: float
240
+ message: str
241
+ extensions: list[Extension]
242
+
243
+ @classmethod
244
+ def from_dict(cls, data: dict[str, Any]) -> AnalysisSuccess:
245
+ return cls(
246
+ id=data["id"],
247
+ elapsed=data["elapsed"],
248
+ message=data["message"],
249
+ extensions=[extension_from_dict(ext) for ext in data["extensions"]],
250
+ )
251
+
252
+
253
+ @dataclass
254
+ class AnalysisError:
255
+ message: str
256
+
257
+
258
+ AnalysisResult = Union[AnalysisSuccess, AnalysisError]
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import json
4
5
  import os
@@ -9,24 +10,23 @@ from contextlib import suppress
9
10
  from dataclasses import asdict, dataclass, field
10
11
  from io import BytesIO
11
12
  from queue import Queue
12
- from typing import Any, TYPE_CHECKING
13
-
14
- import click
13
+ from typing import TYPE_CHECKING, Any
15
14
 
16
15
  from ..cli.handlers import EventHandler
17
16
  from ..runner.events import Initialized, InternalError, Interrupted
18
17
  from . import ci, events, usage
19
18
  from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
20
- from .hosts import HostData
21
19
  from .metadata import Metadata
22
20
  from .models import UploadResponse
23
21
  from .serialization import serialize_event
24
22
 
25
-
26
23
  if TYPE_CHECKING:
27
- from .client import ServiceClient
24
+ import click
25
+
28
26
  from ..cli.context import ExecutionContext
29
27
  from ..runner.events import ExecutionEvent
28
+ from .client import ServiceClient
29
+ from .hosts import HostData
30
30
 
31
31
 
32
32
  @dataclass