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,136 +1,81 @@
1
+ from __future__ import annotations
2
+
1
3
  from contextlib import suppress
4
+ from dataclasses import dataclass
2
5
  from functools import lru_cache
3
- from typing import Any, Dict, Generator, List
6
+ from itertools import cycle, islice
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast, overload
4
8
 
5
9
  import requests
6
- from hypothesis.strategies import SearchStrategy
10
+ from hypothesis_jsonschema import from_schema
7
11
 
8
- from ...constants import DEFAULT_RESPONSE_TIMEOUT
9
- from ...models import APIOperation, Case
10
- from ._hypothesis import PARAMETERS, get_case_strategy
11
- from .constants import LOCATION_TO_CONTAINER
12
+ from schemathesis.config import GenerationConfig
13
+ from schemathesis.core.compat import RefResolutionError, RefResolver
14
+ from schemathesis.core.errors import InfiniteRecursiveReference, UnresolvableReference
15
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
16
+ from schemathesis.core.transforms import deepclone
17
+ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
18
+ from schemathesis.generation.case import Case
19
+ from schemathesis.generation.hypothesis import examples
20
+ from schemathesis.generation.meta import TestPhase
21
+ from schemathesis.schemas import APIOperation
22
+ from schemathesis.specs.openapi.adapter import OpenApiResponses
23
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameter
24
+ from schemathesis.specs.openapi.adapter.security import OpenApiSecurityParameters
25
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
12
26
 
27
+ from ._hypothesis import get_default_format_strategies, openapi_cases
28
+ from .formats import STRING_FORMATS
13
29
 
14
- def get_object_example_from_properties(object_schema: Dict[str, Any]) -> Dict[str, Any]:
15
- return {
16
- prop_name: prop["example"]
17
- for prop_name, prop in object_schema.get("properties", {}).items()
18
- if "example" in prop
19
- }
30
+ if TYPE_CHECKING:
31
+ from hypothesis.strategies import SearchStrategy
20
32
 
33
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
21
34
 
22
- @lru_cache()
23
- def load_external_example(url: str) -> bytes:
24
- """Load examples the `externalValue` keyword."""
25
- response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
26
- response.raise_for_status()
27
- return response.content
28
35
 
36
+ @dataclass
37
+ class ParameterExample:
38
+ """A single example for a named parameter."""
29
39
 
30
- def get_examples(examples: Dict[str, Any]) -> Generator[Any, None, None]:
31
- for example in examples.values():
32
- # IDEA: report when it is not a dictionary
33
- if isinstance(example, dict):
34
- if "value" in example:
35
- yield example["value"]
36
- elif "externalValue" in example:
37
- with suppress(requests.RequestException):
38
- # Report a warning if not available?
39
- yield load_external_example(example["externalValue"])
40
+ container: str
41
+ name: str
42
+ value: Any
40
43
 
44
+ __slots__ = ("container", "name", "value")
41
45
 
42
- def get_parameter_examples(operation_definition: Dict[str, Any], examples_field: str) -> List[Dict[str, Any]]:
43
- """Gets parameter examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
44
- return [
45
- {
46
- "type": LOCATION_TO_CONTAINER.get(parameter["in"]),
47
- "name": parameter["name"],
48
- "examples": list(get_examples(parameter[examples_field])),
49
- }
50
- for parameter in operation_definition.get("parameters", [])
51
- if examples_field in parameter
52
- ]
53
46
 
47
+ @dataclass
48
+ class BodyExample:
49
+ """A single example for a body."""
54
50
 
55
- def get_parameter_example_from_properties(operation_definition: Dict[str, Any]) -> Dict[str, Any]:
56
- static_parameters: Dict[str, Any] = {}
57
- for parameter in operation_definition.get("parameters", []):
58
- parameter_schema = parameter["schema"] if "schema" in parameter else parameter
59
- example = get_object_example_from_properties(parameter_schema)
60
- if example:
61
- parameter_type = LOCATION_TO_CONTAINER[parameter["in"]]
62
- if parameter_type != "body":
63
- if parameter_type not in static_parameters:
64
- static_parameters[parameter_type] = {}
65
- static_parameters[parameter_type][parameter["name"]] = example
66
- else:
67
- # swagger 2 body and formData parameters should not include parameter names
68
- static_parameters[parameter_type] = example
69
- return static_parameters
70
-
71
-
72
- def get_request_body_examples(operation_definition: Dict[str, Any], examples_field: str) -> Dict[str, Any]:
73
- """Gets request body examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
74
- # NOTE. `requestBody` is OAS3-specific. How should it work with OAS2?
75
- request_bodies_items = operation_definition.get("requestBody", {}).get("content", {}).items()
76
- if not request_bodies_items:
77
- return {}
78
- # first element in tuple is media type, second element is dict
79
- _, schema = next(iter(request_bodies_items))
80
- examples = schema.get(examples_field, {})
81
- return {
82
- "type": "body",
83
- "examples": list(get_examples(examples)),
84
- }
85
-
86
-
87
- def get_request_body_example_from_properties(operation_definition: Dict[str, Any]) -> Dict[str, Any]:
88
- static_parameters: Dict[str, Any] = {}
89
- request_bodies_items = operation_definition.get("requestBody", {}).get("content", {}).items()
90
- if request_bodies_items:
91
- _, request_body_schema = next(iter(request_bodies_items))
92
- example = get_object_example_from_properties(request_body_schema.get("schema", {}))
93
- if example:
94
- static_parameters["body"] = example
95
-
96
- return static_parameters
97
-
98
-
99
- def get_static_parameters_from_example(operation: APIOperation) -> Dict[str, Any]:
100
- static_parameters = {}
101
- for name in PARAMETERS:
102
- parameters = getattr(operation, name)
103
- example = parameters.example
104
- if example:
105
- static_parameters[name] = example
106
- return static_parameters
107
-
108
-
109
- def get_static_parameters_from_examples(operation: APIOperation, examples_field: str) -> List[Dict[str, Any]]:
110
- """Get static parameters from OpenAPI examples keyword."""
111
- operation_definition = operation.definition.resolved
112
- return merge_examples(
113
- get_parameter_examples(operation_definition, examples_field),
114
- get_request_body_examples(operation_definition, examples_field),
115
- )
51
+ value: Any
52
+ media_type: str
53
+
54
+ __slots__ = ("value", "media_type")
55
+
56
+
57
+ Example = Union[ParameterExample, BodyExample]
58
+
59
+
60
+ def merge_kwargs(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
61
+ mergeable_keys = {"path_parameters", "headers", "cookies", "query"}
116
62
 
63
+ for key, value in right.items():
64
+ if key in mergeable_keys and key in left:
65
+ if isinstance(left[key], dict) and isinstance(value, dict):
66
+ # kwargs takes precedence
67
+ left[key] = {**left[key], **value}
68
+ continue
69
+ left[key] = value
117
70
 
118
- def get_static_parameters_from_properties(operation: APIOperation) -> Dict[str, Any]:
119
- operation_definition = operation.definition.resolved
120
- return {
121
- **get_parameter_example_from_properties(operation_definition),
122
- **get_request_body_example_from_properties(operation_definition),
123
- }
71
+ return left
124
72
 
125
73
 
126
74
  def get_strategies_from_examples(
127
- operation: APIOperation, examples_field: str = "examples"
128
- ) -> List[SearchStrategy[Case]]:
129
- maps = {}
130
- for location, container in LOCATION_TO_CONTAINER.items():
131
- serializer = operation.get_parameter_serializer(location)
132
- if serializer is not None:
133
- maps[container] = serializer
75
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters], **kwargs: Any
76
+ ) -> list[SearchStrategy[Case]]:
77
+ """Build a set of strategies that generate test cases based on explicit examples in the schema."""
78
+ maps = get_serializers_for_operation(operation)
134
79
 
135
80
  def serialize_components(case: Case) -> Case:
136
81
  """Applies special serialization rules for case components.
@@ -142,64 +87,535 @@ def get_strategies_from_examples(
142
87
  setattr(case, container, map_func(value))
143
88
  return case
144
89
 
145
- strategies = [
146
- get_case_strategy(operation=operation, **static_parameters).map(serialize_components)
147
- for static_parameters in get_static_parameters_from_examples(operation, examples_field)
148
- if static_parameters
90
+ # Extract all top-level examples from the `examples` & `example` fields (`x-` prefixed versions in Open API 2)
91
+ examples = list(extract_top_level(operation))
92
+ # Add examples from parameter's schemas
93
+ examples.extend(extract_from_schemas(operation))
94
+ return [
95
+ openapi_cases(operation=operation, phase=TestPhase.EXAMPLES, **merge_kwargs(parameters, kwargs)).map(
96
+ serialize_components
97
+ )
98
+ for parameters in produce_combinations(examples)
149
99
  ]
150
- for static_parameters in static_parameters_union(
151
- get_static_parameters_from_example(operation), get_static_parameters_from_properties(operation)
152
- ):
153
- strategies.append(get_case_strategy(operation=operation, **static_parameters).map(serialize_components))
154
- return strategies
155
-
156
-
157
- def merge_examples(
158
- parameter_examples: List[Dict[str, Any]], request_body_examples: Dict[str, Any]
159
- ) -> List[Dict[str, Any]]:
160
- """Create list of static parameter objects from the parameter and request body examples."""
161
- static_parameter_list = []
162
- for idx in range(num_examples(parameter_examples, request_body_examples)):
163
- static_parameters: Dict[str, Any] = {}
164
- for parameter in parameter_examples:
165
- container = static_parameters.setdefault(parameter["type"], {})
166
- container[parameter["name"]] = parameter["examples"][min(idx, len(parameter["examples"]) - 1)]
167
- if "examples" in request_body_examples and request_body_examples["examples"]:
168
- static_parameters[request_body_examples["type"]] = request_body_examples["examples"][
169
- min(idx, len(request_body_examples["examples"]) - 1)
170
- ]
171
- static_parameter_list.append(static_parameters)
172
- return static_parameter_list
173
100
 
174
101
 
175
- def static_parameters_union(sp_1: Dict[str, Any], sp_2: Dict[str, Any]) -> List[Dict[str, Any]]:
176
- """Fill missing parameters in each static parameter dict with parameters provided in the other dict."""
177
- full_static_parameters = (_static_parameters_union(sp_1, sp_2), _static_parameters_union(sp_2, sp_1))
178
- return [static_parameter for static_parameter in full_static_parameters if static_parameter]
102
+ def extract_top_level(
103
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
104
+ ) -> Generator[Example, None, None]:
105
+ """Extract top-level parameter examples from `examples` & `example` fields."""
106
+ from .schemas import BaseOpenAPISchema
107
+
108
+ assert isinstance(operation.schema, BaseOpenAPISchema)
109
+
110
+ responses = list(operation.responses.iter_examples())
111
+ for parameter in operation.iter_parameters():
112
+ if "schema" in parameter.definition:
113
+ schema = parameter.definition["schema"]
114
+ resolver = RefResolver.from_schema(schema)
115
+ reference_path: tuple[str, ...] = ()
116
+ definitions = [
117
+ parameter.definition,
118
+ *[
119
+ expanded_schema
120
+ for expanded_schema, _ in _expand_subschemas(
121
+ schema=schema, resolver=resolver, reference_path=reference_path
122
+ )
123
+ ],
124
+ ]
125
+ else:
126
+ definitions = [parameter.definition]
127
+ for definition in definitions:
128
+ # Open API 2 also supports `example`
129
+ for example_keyword in {"example", parameter.adapter.example_keyword}:
130
+ if isinstance(definition, dict) and example_keyword in definition:
131
+ yield ParameterExample(
132
+ container=parameter.location.container_name,
133
+ name=parameter.name,
134
+ value=definition[example_keyword],
135
+ )
136
+ if parameter.adapter.examples_container_keyword in parameter.definition:
137
+ for value in extract_inner_examples(
138
+ parameter.definition[parameter.adapter.examples_container_keyword], operation.schema
139
+ ):
140
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
141
+ if "schema" in parameter.definition:
142
+ schema = parameter.definition["schema"]
143
+ resolver = RefResolver.from_schema(schema)
144
+ reference_path = ()
145
+ for expanded_schema, _ in _expand_subschemas(
146
+ schema=schema, resolver=resolver, reference_path=reference_path
147
+ ):
148
+ if (
149
+ isinstance(expanded_schema, dict)
150
+ and parameter.adapter.examples_container_keyword in expanded_schema
151
+ ):
152
+ for value in expanded_schema[parameter.adapter.examples_container_keyword]:
153
+ yield ParameterExample(
154
+ container=parameter.location.container_name, name=parameter.name, value=value
155
+ )
156
+ for value in find_matching_in_responses(responses, parameter.name):
157
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
158
+ for alternative in operation.body:
159
+ body = cast(OpenApiBody, alternative)
160
+ if "schema" in body.definition:
161
+ schema = body.definition["schema"]
162
+ resolver = RefResolver.from_schema(schema)
163
+ reference_path = ()
164
+ definitions = [
165
+ body.definition,
166
+ *[
167
+ expanded_schema
168
+ for expanded_schema, _ in _expand_subschemas(
169
+ schema=schema, resolver=resolver, reference_path=reference_path
170
+ )
171
+ ],
172
+ ]
173
+ else:
174
+ definitions = [body.definition]
175
+ for definition in definitions:
176
+ # Open API 2 also supports `example`
177
+ for example_keyword in {"example", body.adapter.example_keyword}:
178
+ if isinstance(definition, dict) and example_keyword in definition:
179
+ yield BodyExample(value=definition[example_keyword], media_type=body.media_type)
180
+ if body.adapter.examples_container_keyword in body.definition:
181
+ for value in extract_inner_examples(
182
+ body.definition[body.adapter.examples_container_keyword], operation.schema
183
+ ):
184
+ yield BodyExample(value=value, media_type=body.media_type)
185
+ if "schema" in body.definition:
186
+ schema = body.definition["schema"]
187
+ resolver = RefResolver.from_schema(schema)
188
+ reference_path = ()
189
+ for expanded_schema, _ in _expand_subschemas(
190
+ schema=schema, resolver=resolver, reference_path=reference_path
191
+ ):
192
+ if isinstance(expanded_schema, dict) and body.adapter.examples_container_keyword in expanded_schema:
193
+ for value in expanded_schema[body.adapter.examples_container_keyword]:
194
+ yield BodyExample(value=value, media_type=body.media_type)
195
+
196
+
197
+ @overload
198
+ def _resolve_bundled(
199
+ schema: dict[str, Any], resolver: RefResolver, reference_path: tuple[str, ...]
200
+ ) -> tuple[dict[str, Any], tuple[str, ...]]: ...
201
+
202
+
203
+ @overload
204
+ def _resolve_bundled(
205
+ schema: bool, resolver: RefResolver, reference_path: tuple[str, ...]
206
+ ) -> tuple[bool, tuple[str, ...]]: ...
207
+
208
+
209
+ def _resolve_bundled(
210
+ schema: dict[str, Any] | bool, resolver: RefResolver, reference_path: tuple[str, ...]
211
+ ) -> tuple[dict[str, Any] | bool, tuple[str, ...]]:
212
+ """Resolve $ref if present."""
213
+ if isinstance(schema, dict):
214
+ reference = schema.get("$ref")
215
+ if isinstance(reference, str):
216
+ # Check if this reference is already in the current path
217
+ if reference in reference_path:
218
+ # Real infinite recursive references are caught at the bundling stage.
219
+ # This recursion happens because of how the example phase generates data - it explores everything,
220
+ # so it is the easiest way to break such cycles
221
+ cycle = list(reference_path[reference_path.index(reference) :])
222
+ raise InfiniteRecursiveReference(reference, cycle)
223
+
224
+ new_path = reference_path + (reference,)
225
+
226
+ try:
227
+ _, resolved_schema = resolver.resolve(reference)
228
+ except RefResolutionError as exc:
229
+ raise UnresolvableReference(reference) from exc
230
+
231
+ return resolved_schema, new_path
232
+
233
+ return schema, reference_path
234
+
235
+
236
+ def _expand_subschemas(
237
+ *, schema: dict[str, Any] | bool, resolver: RefResolver, reference_path: tuple[str, ...]
238
+ ) -> Generator[tuple[dict[str, Any] | bool, tuple[str, ...]], None, None]:
239
+ """Expand schema and all its subschemas."""
240
+ try:
241
+ schema, current_path = _resolve_bundled(schema, resolver, reference_path)
242
+ except InfiniteRecursiveReference:
243
+ return
244
+
245
+ yield schema, current_path
246
+
247
+ if isinstance(schema, dict):
248
+ # For anyOf/oneOf, yield each alternative with the same path
249
+ for key in ("anyOf", "oneOf"):
250
+ if key in schema:
251
+ for subschema in schema[key]:
252
+ # Each alternative starts with the current path
253
+ yield subschema, current_path
254
+
255
+ # For allOf, merge all alternatives
256
+ if schema.get("allOf"):
257
+ subschema = deepclone(schema["allOf"][0])
258
+ try:
259
+ subschema, expanded_path = _resolve_bundled(subschema, resolver, current_path)
260
+ except InfiniteRecursiveReference:
261
+ return
262
+
263
+ for sub in schema["allOf"][1:]:
264
+ if isinstance(sub, dict):
265
+ try:
266
+ sub, _ = _resolve_bundled(sub, resolver, current_path)
267
+ except InfiniteRecursiveReference:
268
+ return
269
+ for key, value in sub.items():
270
+ if key == "properties":
271
+ subschema.setdefault("properties", {}).update(value)
272
+ elif key == "required":
273
+ subschema.setdefault("required", []).extend(value)
274
+ elif key == "examples":
275
+ subschema.setdefault("examples", []).extend(value)
276
+ elif key == "example":
277
+ subschema.setdefault("examples", []).append(value)
278
+ else:
279
+ subschema[key] = value
280
+
281
+ # Merge parent schema's fields with the merged allOf result
282
+ # Parent's fields take precedence as they are more specific
283
+ parent_has_example = "example" in schema or "examples" in schema
284
+
285
+ # If parent has examples, remove examples from merged allOf to avoid duplicates
286
+ # The parent's examples were already yielded from the parent schema itself
287
+ if parent_has_example:
288
+ subschema.pop("example", None)
289
+ subschema.pop("examples", None)
290
+
291
+ for key, value in schema.items():
292
+ if key == "allOf":
293
+ # Skip the allOf itself, we already processed it
294
+ continue
295
+ elif key in ("example", "examples"):
296
+ # Skip parent's examples - they were already yielded
297
+ continue
298
+ elif key == "properties":
299
+ # Merge parent properties (parent overrides allOf)
300
+ subschema.setdefault("properties", {}).update(value)
301
+ elif key == "required":
302
+ # Extend required list
303
+ subschema.setdefault("required", []).extend(value)
304
+ else:
305
+ # For other fields, parent value overrides
306
+ subschema[key] = value
307
+
308
+ yield subschema, expanded_path
309
+
310
+
311
+ def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
312
+ """Extract exact examples values from the `examples` dictionary."""
313
+ if isinstance(examples, dict):
314
+ for example in examples.values():
315
+ if isinstance(example, dict):
316
+ if "$ref" in example:
317
+ _, example = schema.resolver.resolve(example["$ref"])
318
+ if "value" in example:
319
+ yield example["value"]
320
+ elif "externalValue" in example:
321
+ with suppress(requests.RequestException):
322
+ # Report a warning if not available?
323
+ yield load_external_example(example["externalValue"])
324
+ elif example:
325
+ yield example
326
+ elif isinstance(examples, list):
327
+ yield from examples
328
+
329
+
330
+ @lru_cache
331
+ def load_external_example(url: str) -> bytes:
332
+ """Load examples the `externalValue` keyword."""
333
+ response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
334
+ response.raise_for_status()
335
+ return response.content
179
336
 
180
337
 
181
- def _static_parameters_union(base_obj: Dict[str, Any], fill_obj: Dict[str, Any]) -> Dict[str, Any]:
182
- """Fill base_obj with parameter examples in fill_obj that were not in base_obj."""
183
- if not base_obj:
184
- return {}
338
+ def extract_from_schemas(
339
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
340
+ ) -> Generator[Example, None, None]:
341
+ """Extract examples from parameters' schema definitions."""
342
+ for parameter in operation.iter_parameters():
343
+ schema = parameter.optimized_schema
344
+ if isinstance(schema, bool):
345
+ continue
346
+ resolver = RefResolver.from_schema(schema)
347
+ reference_path: tuple[str, ...] = ()
348
+ bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
349
+ for value in extract_from_schema(
350
+ operation=operation,
351
+ schema=schema,
352
+ example_keyword=parameter.adapter.example_keyword,
353
+ examples_container_keyword=parameter.adapter.examples_container_keyword,
354
+ resolver=resolver,
355
+ reference_path=reference_path,
356
+ bundle_storage=bundle_storage,
357
+ ):
358
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
359
+ for alternative in operation.body:
360
+ body = cast(OpenApiBody, alternative)
361
+ schema = body.optimized_schema
362
+ if isinstance(schema, bool):
363
+ continue
364
+ resolver = RefResolver.from_schema(schema)
365
+ bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
366
+ for example_keyword, examples_container_keyword in (("example", "examples"), ("x-example", "x-examples")):
367
+ reference_path = ()
368
+ for value in extract_from_schema(
369
+ operation=operation,
370
+ schema=schema,
371
+ example_keyword=example_keyword,
372
+ examples_container_keyword=examples_container_keyword,
373
+ resolver=resolver,
374
+ reference_path=reference_path,
375
+ bundle_storage=bundle_storage,
376
+ ):
377
+ yield BodyExample(value=value, media_type=body.media_type)
378
+
379
+
380
+ def extract_from_schema(
381
+ *,
382
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
383
+ schema: dict[str, Any],
384
+ example_keyword: str,
385
+ examples_container_keyword: str,
386
+ resolver: RefResolver,
387
+ reference_path: tuple[str, ...],
388
+ bundle_storage: dict[str, Any] | None,
389
+ ) -> Generator[Any, None, None]:
390
+ """Extract all examples from a single schema definition."""
391
+ # This implementation supports only `properties` and `items`
392
+ try:
393
+ schema, current_path = _resolve_bundled(schema, resolver, reference_path)
394
+ except InfiniteRecursiveReference:
395
+ return
396
+
397
+ # If schema has allOf, we need to get merged properties from allOf items
398
+ # to extract property-level examples from all schemas, not just the parent
399
+ properties_to_process = schema.get("properties", {})
400
+ if "allOf" in schema and "properties" in schema:
401
+ # Get the merged allOf schema which includes properties from all allOf items
402
+ for expanded_schema, _ in _expand_subschemas(schema=schema, resolver=resolver, reference_path=current_path):
403
+ if expanded_schema is not schema and isinstance(expanded_schema, dict):
404
+ # This is the merged allOf result with combined properties
405
+ if "properties" in expanded_schema:
406
+ properties_to_process = expanded_schema["properties"]
407
+ break
408
+
409
+ if properties_to_process:
410
+ variants = {}
411
+ required = schema.get("required", [])
412
+ to_generate: dict[str, Any] = {}
413
+
414
+ for name, subschema in list(properties_to_process.items()):
415
+ values = []
416
+ for expanded_schema, expanded_path in _expand_subschemas(
417
+ schema=subschema, resolver=resolver, reference_path=current_path
418
+ ):
419
+ if isinstance(expanded_schema, bool):
420
+ to_generate[name] = expanded_schema
421
+ continue
422
+
423
+ if example_keyword in expanded_schema:
424
+ values.append(expanded_schema[example_keyword])
425
+
426
+ if examples_container_keyword in expanded_schema and isinstance(
427
+ expanded_schema[examples_container_keyword], list
428
+ ):
429
+ # These are JSON Schema examples, which is an array of values
430
+ values.extend(expanded_schema[examples_container_keyword])
431
+
432
+ # Check nested examples as well
433
+ values.extend(
434
+ extract_from_schema(
435
+ operation=operation,
436
+ schema=expanded_schema,
437
+ example_keyword=example_keyword,
438
+ examples_container_keyword=examples_container_keyword,
439
+ resolver=resolver,
440
+ reference_path=expanded_path,
441
+ bundle_storage=bundle_storage,
442
+ )
443
+ )
444
+
445
+ if not values:
446
+ if name in required:
447
+ # Defer generation to only generate these variants if at least one property has examples
448
+ to_generate[name] = expanded_schema
449
+ continue
450
+
451
+ variants[name] = values
452
+
453
+ if variants:
454
+ config = operation.schema.config.generation_for(operation=operation, phase="examples")
455
+ for name, subschema in to_generate.items():
456
+ if name in variants:
457
+ # Generated by one of `anyOf` or similar sub-schemas
458
+ continue
459
+ if bundle_storage is not None:
460
+ subschema = dict(subschema)
461
+ subschema[BUNDLE_STORAGE_KEY] = bundle_storage
462
+ generated = _generate_single_example(subschema, config)
463
+ variants[name] = [generated]
464
+
465
+ # Calculate the maximum number of examples any property has
466
+ total_combos = max(len(examples) for examples in variants.values())
467
+ # Evenly distribute examples by cycling through them
468
+ for idx in range(total_combos):
469
+ yield {
470
+ name: next(islice(cycle(property_variants), idx, None))
471
+ for name, property_variants in variants.items()
472
+ }
473
+
474
+ elif "items" in schema and isinstance(schema["items"], dict):
475
+ # Each inner value should be wrapped in an array
476
+ for value in extract_from_schema(
477
+ operation=operation,
478
+ schema=schema["items"],
479
+ example_keyword=example_keyword,
480
+ examples_container_keyword=examples_container_keyword,
481
+ resolver=resolver,
482
+ reference_path=current_path,
483
+ bundle_storage=bundle_storage,
484
+ ):
485
+ yield [value]
486
+
487
+
488
+ def _generate_single_example(
489
+ schema: dict[str, Any],
490
+ generation_config: GenerationConfig,
491
+ ) -> Any:
492
+ strategy = from_schema(
493
+ schema,
494
+ custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
495
+ allow_x00=generation_config.allow_x00,
496
+ codec=generation_config.codec,
497
+ )
498
+ return examples.generate_one(strategy)
499
+
500
+
501
+ def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
502
+ """Generate a minimal set of combinations for the given list of parameters."""
503
+ # Split regular parameters & body variants first
504
+ parameters: dict[str, dict[str, list]] = {}
505
+ bodies: dict[str, list] = {}
506
+ for example in examples:
507
+ if isinstance(example, ParameterExample):
508
+ container_examples = parameters.setdefault(example.container, {})
509
+ parameter_examples = container_examples.setdefault(example.name, [])
510
+ parameter_examples.append(example.value)
511
+ else:
512
+ values = bodies.setdefault(example.media_type, [])
513
+ values.append(example.value)
514
+
515
+ if bodies:
516
+ if parameters:
517
+ parameter_combos = list(_produce_parameter_combinations(parameters))
518
+ body_combos = [
519
+ {"media_type": media_type, "body": value} for media_type, values in bodies.items() for value in values
520
+ ]
521
+ total_combos = max(len(parameter_combos), len(body_combos))
522
+ for idx in range(total_combos):
523
+ yield {
524
+ **next(islice(cycle(body_combos), idx, None)),
525
+ **next(islice(cycle(parameter_combos), idx, None)),
526
+ }
527
+ else:
528
+ for media_type, values in bodies.items():
529
+ for body in values:
530
+ yield {"media_type": media_type, "body": body}
531
+ elif parameters:
532
+ yield from _produce_parameter_combinations(parameters)
533
+
534
+
535
+ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> Generator[dict[str, Any], None, None]:
536
+ total_combos = max(
537
+ len(variants) for container_variants in parameters.values() for variants in container_variants.values()
538
+ )
539
+ for idx in range(total_combos):
540
+ yield {
541
+ container: {
542
+ name: next(islice(cycle(parameter_variants), idx, None))
543
+ for name, parameter_variants in variants.items()
544
+ }
545
+ for container, variants in parameters.items()
546
+ }
185
547
 
186
- full_static_parameters: Dict[str, Any] = {**base_obj}
187
548
 
188
- for parameter_type, examples in fill_obj.items():
189
- if parameter_type not in full_static_parameters:
190
- full_static_parameters[parameter_type] = examples
191
- elif parameter_type != "body":
192
- # copy individual parameter names.
193
- # body is unnamed, single examples, so we only do this for named parameters.
194
- for parameter_name, example in examples.items():
195
- if parameter_name not in full_static_parameters[parameter_type]:
196
- full_static_parameters[parameter_type][parameter_name] = example
197
- return full_static_parameters
549
+ NOT_FOUND = object()
198
550
 
199
551
 
200
- def num_examples(parameter_examples: List[Dict[str, Any]], request_body_examples: Dict[str, Any]) -> int:
201
- max_parameter_examples = (
202
- max(len(parameter["examples"]) for parameter in parameter_examples) if parameter_examples else 0
203
- )
204
- num_request_body_examples = len(request_body_examples["examples"]) if "examples" in request_body_examples else 0
205
- return max(max_parameter_examples, num_request_body_examples)
552
+ def find_matching_in_responses(examples: list[tuple[str, object]], param: str) -> Iterator[Any]:
553
+ """Find matching parameter examples."""
554
+ normalized = param.lower()
555
+ is_id_param = normalized.endswith("id")
556
+ # Extract values from response examples that match input parameters.
557
+ # E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
558
+ # as examples for the "id" path parameter.
559
+ for schema_name, example in examples:
560
+ if not isinstance(example, dict):
561
+ continue
562
+ # Unwrapping example from `{"item": [{...}]}`
563
+ if isinstance(example, dict):
564
+ inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
565
+ if inner is not None:
566
+ if isinstance(inner, list):
567
+ for sub_example in inner:
568
+ if isinstance(sub_example, dict):
569
+ for found in _find_matching_in_responses(
570
+ sub_example, schema_name, param, normalized, is_id_param
571
+ ):
572
+ if found is not NOT_FOUND:
573
+ yield found
574
+ continue
575
+ if isinstance(inner, dict):
576
+ example = inner
577
+ for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
578
+ if found is not NOT_FOUND:
579
+ yield found
580
+
581
+
582
+ def _find_matching_in_responses(
583
+ example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
584
+ ) -> Iterator[Any]:
585
+ # Check for exact match
586
+ if param in example:
587
+ yield example[param]
588
+ return
589
+ if is_id_param and param[:-2] in example:
590
+ value = example[param[:-2]]
591
+ if isinstance(value, list):
592
+ for sub_example in value:
593
+ for found in _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param):
594
+ if found is not NOT_FOUND:
595
+ yield found
596
+ return
597
+ else:
598
+ yield value
599
+ return
600
+
601
+ # Check for case-insensitive match
602
+ for key in example:
603
+ if key.lower() == normalized:
604
+ yield example[key]
605
+ return
606
+ else:
607
+ # If no match found and it's an ID parameter, try additional checks
608
+ if is_id_param:
609
+ # Check for 'id' if parameter is '{something}Id'
610
+ if "id" in example:
611
+ yield example["id"]
612
+ return
613
+ # Check for '{schemaName}Id' or '{schemaName}_id'
614
+ if normalized == "id" or normalized.startswith(schema_name.lower()):
615
+ for key in (schema_name, schema_name.lower()):
616
+ for suffix in ("_id", "Id"):
617
+ with_suffix = f"{key}{suffix}"
618
+ if with_suffix in example:
619
+ yield example[with_suffix]
620
+ return
621
+ yield NOT_FOUND