schemathesis 3.25.5__py3-none-any.whl → 3.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. schemathesis/_dependency_versions.py +1 -0
  2. schemathesis/_hypothesis.py +1 -0
  3. schemathesis/_xml.py +1 -0
  4. schemathesis/auths.py +1 -0
  5. schemathesis/cli/__init__.py +39 -37
  6. schemathesis/cli/cassettes.py +4 -4
  7. schemathesis/cli/context.py +6 -0
  8. schemathesis/cli/output/default.py +185 -45
  9. schemathesis/cli/output/short.py +8 -0
  10. schemathesis/experimental/__init__.py +7 -0
  11. schemathesis/filters.py +1 -0
  12. schemathesis/models.py +5 -2
  13. schemathesis/parameters.py +1 -0
  14. schemathesis/runner/__init__.py +36 -9
  15. schemathesis/runner/events.py +33 -1
  16. schemathesis/runner/impl/core.py +99 -23
  17. schemathesis/{cli → runner}/probes.py +32 -21
  18. schemathesis/runner/serialization.py +4 -2
  19. schemathesis/schemas.py +1 -0
  20. schemathesis/serializers.py +11 -3
  21. schemathesis/service/client.py +35 -2
  22. schemathesis/service/extensions.py +224 -0
  23. schemathesis/service/hosts.py +1 -0
  24. schemathesis/service/metadata.py +24 -0
  25. schemathesis/service/models.py +210 -2
  26. schemathesis/service/serialization.py +44 -1
  27. schemathesis/specs/openapi/__init__.py +1 -0
  28. schemathesis/specs/openapi/_hypothesis.py +9 -1
  29. schemathesis/specs/openapi/examples.py +22 -24
  30. schemathesis/specs/openapi/expressions/__init__.py +1 -0
  31. schemathesis/specs/openapi/expressions/lexer.py +1 -0
  32. schemathesis/specs/openapi/expressions/nodes.py +1 -0
  33. schemathesis/specs/openapi/links.py +1 -0
  34. schemathesis/specs/openapi/media_types.py +34 -0
  35. schemathesis/specs/openapi/negative/mutations.py +1 -0
  36. schemathesis/specs/openapi/schemas.py +10 -3
  37. schemathesis/specs/openapi/security.py +5 -1
  38. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
  39. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/RECORD +42 -40
  40. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
  41. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
  42. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
+ import json
2
3
  import hashlib
3
4
  import http
4
5
  from dataclasses import asdict
5
- from typing import Any
6
+ from typing import Any, TYPE_CHECKING
6
7
  from urllib.parse import urljoin
7
8
 
8
9
  import requests
@@ -11,8 +12,11 @@ from requests.adapters import HTTPAdapter, Retry
11
12
  from ..constants import USER_AGENT
12
13
  from .ci import CIProvider
13
14
  from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
14
- from .metadata import Metadata
15
+ from .metadata import Metadata, collect_dependency_versions
15
16
  from .models import (
17
+ AnalysisSuccess,
18
+ AnalysisError,
19
+ AnalysisResult,
16
20
  ProjectDetails,
17
21
  AuthResponse,
18
22
  FailedUploadResponse,
@@ -23,6 +27,10 @@ from .models import (
23
27
  )
24
28
 
25
29
 
30
+ if TYPE_CHECKING:
31
+ from ..runner import probes
32
+
33
+
26
34
  def response_hook(response: requests.Response, **_kwargs: Any) -> None:
27
35
  if response.status_code != http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
28
36
  response.raise_for_status()
@@ -98,3 +106,28 @@ class ServiceClient(requests.Session):
98
106
  if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
99
107
  return FailedUploadResponse(detail=data["detail"])
100
108
  return UploadResponse(message=data["message"], next_url=data["next"], correlation_id=data["correlation_id"])
109
+
110
+ def analyze_schema(self, probes: list[probes.ProbeRun] | None, schema: dict[str, Any]) -> AnalysisResult:
111
+ """Analyze the API schema."""
112
+ # Manual serialization reduces the size of the payload a bit
113
+ dependencies = collect_dependency_versions()
114
+ if probes is not None:
115
+ _probes = [probe.serialize() for probe in probes]
116
+ else:
117
+ _probes = []
118
+ content = json.dumps(
119
+ {
120
+ "probes": _probes,
121
+ "schema": schema,
122
+ "dependencies": list(map(asdict, dependencies)),
123
+ },
124
+ separators=(",", ":"),
125
+ )
126
+ response = self.post("/cli/analysis/", data=content, headers={"Content-Type": "application/json"}, timeout=None)
127
+ if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
128
+ try:
129
+ message = response.json()["detail"]
130
+ except json.JSONDecodeError:
131
+ message = response.text
132
+ return AnalysisError(message=message)
133
+ return AnalysisSuccess.from_dict(response.json())
@@ -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, Callable, Optional, Any
7
+
8
+ from ..graphql import nodes
9
+ from ..internal.result import Result, Ok, Err
10
+ from .models import (
11
+ Extension,
12
+ SchemaPatchesExtension,
13
+ StrategyDefinition,
14
+ OpenApiStringFormatsExtension,
15
+ GraphQLScalarsExtension,
16
+ MediaTypesExtension,
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) -> Optional[st.SearchStrategy]:
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 ..utils 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 ..specs.openapi._hypothesis 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,4 +1,5 @@
1
1
  """Work with stored auth data."""
2
+
2
3
  from __future__ import annotations
3
4
  import enum
4
5
  import tempfile
@@ -1,7 +1,9 @@
1
1
  """Useful info to collect from CLI usage."""
2
+
2
3
  from __future__ import annotations
3
4
  import os
4
5
  import platform
6
+ from importlib import metadata
5
7
  from dataclasses import dataclass, field
6
8
 
7
9
  from ..constants import SCHEMATHESIS_VERSION
@@ -32,6 +34,27 @@ class CliMetadata:
32
34
  version: str = SCHEMATHESIS_VERSION
33
35
 
34
36
 
37
+ DEPDENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
38
+
39
+
40
+ @dataclass
41
+ class Dependency:
42
+ """A single dependency."""
43
+
44
+ # Name of the package.
45
+ name: str
46
+ # Version of the package.
47
+ version: str
48
+
49
+ @classmethod
50
+ def from_name(cls, name: str) -> Dependency:
51
+ return cls(name=name, version=metadata.version(name))
52
+
53
+
54
+ def collect_dependency_versions() -> list[Dependency]:
55
+ return [Dependency.from_name(name) for name in DEPDENDENCY_NAMES]
56
+
57
+
35
58
  @dataclass
36
59
  class Metadata:
37
60
  """CLI environment metadata."""
@@ -44,3 +67,4 @@ class Metadata:
44
67
  cli: CliMetadata = field(default_factory=CliMetadata)
45
68
  # Used Docker image if any
46
69
  docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
70
+ depdenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
  from enum import Enum
4
- from typing import Any
4
+ from typing import Any, Iterable, TypedDict, Union, Literal
5
5
 
6
6
 
7
7
  class UploadSource(str, Enum):
@@ -47,3 +47,211 @@ class UploadResponse:
47
47
  @dataclass
48
48
  class FailedUploadResponse:
49
49
  detail: str
50
+
51
+
52
+ @dataclass
53
+ class NotAppliedState:
54
+ """The extension was not applied."""
55
+
56
+ def __str__(self) -> str:
57
+ return "Not Applied"
58
+
59
+
60
+ @dataclass
61
+ class SuccessState:
62
+ """The extension was applied successfully."""
63
+
64
+ def __str__(self) -> str:
65
+ return "Success"
66
+
67
+
68
+ @dataclass
69
+ class ErrorState:
70
+ """An error occurred during the extension application."""
71
+
72
+ errors: list[str] = field(default_factory=list)
73
+ exceptions: list[Exception] = field(default_factory=list)
74
+
75
+ def __str__(self) -> str:
76
+ return "Error"
77
+
78
+
79
+ ExtensionState = Union[NotAppliedState, SuccessState, ErrorState]
80
+
81
+
82
+ @dataclass
83
+ class BaseExtension:
84
+ def set_state(self, state: ExtensionState) -> None:
85
+ self.state = state
86
+
87
+ def set_success(self) -> None:
88
+ self.set_state(SuccessState())
89
+
90
+ def set_error(self, errors: list[str] | None = None, exceptions: list[Exception] | None = None) -> None:
91
+ self.set_state(ErrorState(errors=errors or [], exceptions=exceptions or []))
92
+
93
+
94
+ @dataclass
95
+ class UnknownExtension(BaseExtension):
96
+ """An unknown extension.
97
+
98
+ Likely the CLI should be updated.
99
+ """
100
+
101
+ type: str
102
+ state: ExtensionState = field(default_factory=NotAppliedState)
103
+
104
+ @property
105
+ def summary(self) -> str:
106
+ return f"`{self.type}`"
107
+
108
+
109
+ class AddPatch(TypedDict):
110
+ operation: Literal["add"]
111
+ path: list[str | int]
112
+ value: Any
113
+
114
+
115
+ class RemovePatch(TypedDict):
116
+ operation: Literal["remove"]
117
+ path: list[str | int]
118
+
119
+
120
+ Patch = Union[AddPatch, RemovePatch]
121
+
122
+
123
+ @dataclass
124
+ class SchemaPatchesExtension(BaseExtension):
125
+ """Update the schema with its optimized version."""
126
+
127
+ patches: list[Patch]
128
+ state: ExtensionState = field(default_factory=NotAppliedState)
129
+
130
+ @property
131
+ def summary(self) -> str:
132
+ count = len(self.patches)
133
+ plural = "es" if count > 1 else ""
134
+ return f"{count} schema patch{plural}"
135
+
136
+
137
+ class TransformFunctionDefinition(TypedDict):
138
+ kind: Literal["map", "filter"]
139
+ name: str
140
+ arguments: dict[str, Any]
141
+
142
+
143
+ @dataclass
144
+ class StrategyDefinition:
145
+ name: str
146
+ transforms: list[TransformFunctionDefinition] | None = None
147
+ arguments: dict[str, Any] | None = None
148
+
149
+
150
+ def _strategies_from_definition(items: dict[str, list[dict[str, Any]]]) -> dict[str, list[StrategyDefinition]]:
151
+ return {name: [StrategyDefinition(**item) for item in value] for name, value in items.items()}
152
+
153
+
154
+ def _format_items(items: Iterable[str]) -> str:
155
+ return ", ".join([f"`{item}`" for item in items])
156
+
157
+
158
+ @dataclass
159
+ class OpenApiStringFormatsExtension(BaseExtension):
160
+ """Custom string formats."""
161
+
162
+ formats: dict[str, list[StrategyDefinition]]
163
+ state: ExtensionState = field(default_factory=NotAppliedState)
164
+
165
+ @classmethod
166
+ def from_dict(cls, formats: dict[str, list[dict[str, Any]]]) -> OpenApiStringFormatsExtension:
167
+ return cls(formats=_strategies_from_definition(formats))
168
+
169
+ @property
170
+ def summary(self) -> str:
171
+ count = len(self.formats)
172
+ plural = "s" if count > 1 else ""
173
+ formats = _format_items(self.formats)
174
+ return f"Data generator{plural} for {formats} Open API format{plural}"
175
+
176
+
177
+ @dataclass
178
+ class GraphQLScalarsExtension(BaseExtension):
179
+ """Custom scalars."""
180
+
181
+ scalars: dict[str, list[StrategyDefinition]]
182
+ state: ExtensionState = field(default_factory=NotAppliedState)
183
+
184
+ @classmethod
185
+ def from_dict(cls, scalars: dict[str, list[dict[str, Any]]]) -> GraphQLScalarsExtension:
186
+ return cls(scalars=_strategies_from_definition(scalars))
187
+
188
+ @property
189
+ def summary(self) -> str:
190
+ count = len(self.scalars)
191
+ plural = "s" if count > 1 else ""
192
+ scalars = _format_items(self.scalars)
193
+ return f"Data generator{plural} for {scalars} GraphQL scalar{plural}"
194
+
195
+
196
+ @dataclass
197
+ class MediaTypesExtension(BaseExtension):
198
+ media_types: dict[str, list[StrategyDefinition]]
199
+ state: ExtensionState = field(default_factory=NotAppliedState)
200
+
201
+ @classmethod
202
+ def from_dict(cls, media_types: dict[str, list[dict[str, Any]]]) -> MediaTypesExtension:
203
+ return cls(media_types=_strategies_from_definition(media_types))
204
+
205
+ @property
206
+ def summary(self) -> str:
207
+ count = len(self.media_types)
208
+ plural = "s" if count > 1 else ""
209
+ media_types = _format_items(self.media_types)
210
+ return f"Data generator{plural} for {media_types} media type{plural}"
211
+
212
+
213
+ # A CLI extension that can be used to adjust the behavior of Schemathesis.
214
+ Extension = Union[
215
+ SchemaPatchesExtension,
216
+ OpenApiStringFormatsExtension,
217
+ GraphQLScalarsExtension,
218
+ MediaTypesExtension,
219
+ UnknownExtension,
220
+ ]
221
+
222
+
223
+ def extension_from_dict(data: dict[str, Any]) -> Extension:
224
+ if data["type"] == "schema_patches":
225
+ return SchemaPatchesExtension(patches=data["patches"])
226
+ elif data["type"] == "string_formats":
227
+ return OpenApiStringFormatsExtension.from_dict(formats=data["items"])
228
+ elif data["type"] == "scalars":
229
+ return GraphQLScalarsExtension.from_dict(scalars=data["items"])
230
+ elif data["type"] == "media_types":
231
+ return MediaTypesExtension.from_dict(media_types=data["items"])
232
+ return UnknownExtension(type=data["type"])
233
+
234
+
235
+ @dataclass
236
+ class AnalysisSuccess:
237
+ id: str
238
+ elapsed: float
239
+ message: str
240
+ extensions: list[Extension]
241
+
242
+ @classmethod
243
+ def from_dict(cls, data: dict[str, Any]) -> AnalysisSuccess:
244
+ return cls(
245
+ id=data["id"],
246
+ elapsed=data["elapsed"],
247
+ message=data["message"],
248
+ extensions=[extension_from_dict(ext) for ext in data["extensions"]],
249
+ )
250
+
251
+
252
+ @dataclass
253
+ class AnalysisError:
254
+ message: str
255
+
256
+
257
+ AnalysisResult = Union[AnalysisSuccess, AnalysisError]
@@ -1,11 +1,15 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import asdict
3
4
  from typing import Any, Callable, Dict, Optional, TypeVar, cast
4
5
 
6
+ from ..exceptions import format_exception
7
+ from ..internal.result import Err, Ok
8
+ from ..internal.transformation import merge_recursively
5
9
  from ..models import Response
10
+ from .models import AnalysisSuccess
6
11
  from ..runner import events
7
12
  from ..runner.serialization import SerializedCase
8
- from ..internal.transformation import merge_recursively
9
13
 
10
14
  S = TypeVar("S", bound=events.ExecutionEvent)
11
15
  SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
@@ -19,6 +23,33 @@ def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
19
23
  }
20
24
 
21
25
 
26
+ def serialize_before_probing(_: events.BeforeProbing) -> None:
27
+ return None
28
+
29
+
30
+ def serialize_after_probing(event: events.AfterProbing) -> dict[str, Any] | None:
31
+ probes = event.probes or []
32
+ return {"probes": [probe.serialize() for probe in probes]}
33
+
34
+
35
+ def serialize_before_analysis(_: events.BeforeAnalysis) -> None:
36
+ return None
37
+
38
+
39
+ def serialize_after_analysis(event: events.AfterAnalysis) -> dict[str, Any] | None:
40
+ data = {}
41
+ analysis = event.analysis
42
+ if isinstance(analysis, Ok):
43
+ result = analysis.ok()
44
+ if isinstance(result, AnalysisSuccess):
45
+ data["analysis_id"] = result.id
46
+ else:
47
+ data["error"] = result.message
48
+ elif isinstance(analysis, Err):
49
+ data["error"] = format_exception(analysis.err())
50
+ return data
51
+
52
+
22
53
  def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
23
54
  return {
24
55
  "correlation_id": event.correlation_id,
@@ -116,6 +147,10 @@ def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
116
147
 
117
148
  SERIALIZER_MAP = {
118
149
  events.Initialized: serialize_initialized,
150
+ events.BeforeProbing: serialize_before_probing,
151
+ events.AfterProbing: serialize_after_probing,
152
+ events.BeforeAnalysis: serialize_before_analysis,
153
+ events.AfterAnalysis: serialize_after_analysis,
119
154
  events.BeforeExecution: serialize_before_execution,
120
155
  events.AfterExecution: serialize_after_execution,
121
156
  events.Interrupted: serialize_interrupted,
@@ -128,6 +163,10 @@ def serialize_event(
128
163
  event: events.ExecutionEvent,
129
164
  *,
130
165
  on_initialized: SerializeFunc | None = None,
166
+ on_before_probing: SerializeFunc | None = None,
167
+ on_after_probing: SerializeFunc | None = None,
168
+ on_before_analysis: SerializeFunc | None = None,
169
+ on_after_analysis: SerializeFunc | None = None,
131
170
  on_before_execution: SerializeFunc | None = None,
132
171
  on_after_execution: SerializeFunc | None = None,
133
172
  on_interrupted: SerializeFunc | None = None,
@@ -139,6 +178,10 @@ def serialize_event(
139
178
  # Use the explicitly provided serializer for this event and fallback to default one if it is not provided
140
179
  serializer = {
141
180
  events.Initialized: on_initialized,
181
+ events.BeforeProbing: on_before_probing,
182
+ events.AfterProbing: on_after_probing,
183
+ events.BeforeAnalysis: on_before_analysis,
184
+ events.AfterAnalysis: on_after_analysis,
142
185
  events.BeforeExecution: on_before_execution,
143
186
  events.AfterExecution: on_after_execution,
144
187
  events.Interrupted: on_interrupted,
@@ -1,3 +1,4 @@
1
1
  from .formats import register_string_format as format
2
2
  from .formats import unregister_string_format
3
3
  from .loaders import from_aiohttp, from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
4
+ from .media_types import register_media_type as media_type
@@ -30,6 +30,7 @@ from ...types import NotSet
30
30
  from ...serializers import Binary
31
31
  from ...utils import compose, skip
32
32
  from .constants import LOCATION_TO_CONTAINER
33
+ from .media_types import MEDIA_TYPES
33
34
  from .negative import negative_schema
34
35
  from .negative.utils import can_negate
35
36
  from .parameters import OpenAPIBody, parameters_to_json_schema
@@ -134,7 +135,7 @@ def get_case_strategy(
134
135
 
135
136
  context = HookContext(operation)
136
137
 
137
- generation_config = generation_config or GenerationConfig()
138
+ generation_config = generation_config or operation.schema.generation_config
138
139
 
139
140
  path_parameters_ = generate_parameter(
140
141
  "path", path_parameters, operation, draw, context, hooks, generator, generation_config
@@ -175,6 +176,11 @@ def get_case_strategy(
175
176
  else:
176
177
  body_ = ValueContainer(value=body, location="body", generator=None)
177
178
  else:
179
+ # This explicit body payload comes for a media type that has a custom strategy registered
180
+ # Such strategies only support binary payloads, otherwise they can't be serialized
181
+ if not isinstance(body, bytes) and media_type in MEDIA_TYPES:
182
+ all_media_types = operation.get_request_payload_content_types()
183
+ raise SerializationNotPossible.from_media_types(*all_media_types)
178
184
  body_ = ValueContainer(value=body, location="body", generator=None)
179
185
 
180
186
  if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
@@ -212,6 +218,8 @@ def _get_body_strategy(
212
218
  operation: APIOperation,
213
219
  generation_config: GenerationConfig,
214
220
  ) -> st.SearchStrategy:
221
+ if parameter.media_type in MEDIA_TYPES:
222
+ return MEDIA_TYPES[parameter.media_type]
215
223
  # The cache key relies on object ids, which means that the parameter should not be mutated
216
224
  # Note, the parent schema is not included as each parameter belong only to one schema
217
225
  if parameter in _BODY_STRATEGIES_CACHE and strategy_factory in _BODY_STRATEGIES_CACHE[parameter]: